From cce4a41c5d97e2aec83e6e1095424b3cd3a13e3b Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 10 Oct 2021 19:33:45 -0700 Subject: [PATCH 01/79] Add "disabled" common string --- osu.Game/Localisation/CommonStrings.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 33a6eb5d58..3ea337c279 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -24,6 +24,11 @@ namespace osu.Game.Localisation /// public static LocalisableString Enabled => new TranslatableString(getKey(@"enabled"), @"Enabled"); + /// + /// "Disabled" + /// + public static LocalisableString Disabled => new TranslatableString(getKey(@"disabled"), @"Disabled"); + /// /// "Default" /// From 545cfc7bf1e2b84304e28caf381e200dcb10d59f Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 10 Oct 2021 19:35:25 -0700 Subject: [PATCH 02/79] Localise tracked setting toasts --- osu.Game/Configuration/OsuConfigManager.cs | 10 ++++++---- osu.Game/Overlays/OSD/Toast.cs | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 9b0d7f51da..05a18ef88a 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -6,10 +6,12 @@ using System.Diagnostics; using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Input; using osu.Game.Input.Bindings; +using osu.Game.Localisation; using osu.Game.Overlays; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select; @@ -185,13 +187,13 @@ namespace osu.Game.Configuration return new TrackedSettings { - new TrackedSetting(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, "gameplay mouse buttons", v ? "disabled" : "enabled", LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons))), - new TrackedSetting(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, "HUD Visibility", m.GetDescription(), $"cycle: {LookupKeyBindings(GlobalAction.ToggleInGameInterface)} quick view: {LookupKeyBindings(GlobalAction.HoldForHUD)}")), - new TrackedSetting(OsuSetting.Scaling, m => new SettingDescription(m, "scaling", m.GetDescription())), + new TrackedSetting(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, GlobalActionKeyBindingStrings.ToggleGameplayMouseButtons, v ? CommonStrings.Disabled.ToLower() : CommonStrings.Enabled.ToLower(), LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons))), + new TrackedSetting(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, GameplaySettingsStrings.HUDVisibilityMode, m.GetLocalisableDescription(), $"{GlobalActionKeyBindingStrings.ToggleInGameInterface}: {LookupKeyBindings(GlobalAction.ToggleInGameInterface)} {GlobalActionKeyBindingStrings.HoldForHUD}: {LookupKeyBindings(GlobalAction.HoldForHUD)}")), + new TrackedSetting(OsuSetting.Scaling, m => new SettingDescription(m, GraphicsSettingsStrings.ScreenScaling, m.GetLocalisableDescription())), new TrackedSetting(OsuSetting.Skin, m => { string skinName = LookupSkinName(m) ?? string.Empty; - return new SettingDescription(skinName, "skin", skinName, $"random: {LookupKeyBindings(GlobalAction.RandomSkin)}"); + return new SettingDescription(skinName, SkinSettingsStrings.SkinSectionHeader, skinName, $"{GlobalActionKeyBindingStrings.RandomSkin}: {LookupKeyBindings(GlobalAction.RandomSkin)}"); }) }; } diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs index 4a6316df3f..4221bb1069 100644 --- a/osu.Game/Overlays/OSD/Toast.cs +++ b/osu.Game/Overlays/OSD/Toast.cs @@ -1,9 +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.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; @@ -23,7 +25,7 @@ namespace osu.Game.Overlays.OSD protected readonly OsuSpriteText ShortcutText; - protected Toast(string description, string value, string shortcut) + protected Toast(LocalisableString description, LocalisableString value, LocalisableString shortcut) { Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -60,7 +62,7 @@ namespace osu.Game.Overlays.OSD Spacing = new Vector2(1, 0), Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = description.ToUpperInvariant() + Text = description.ToUpper() }, ValueText = new OsuSpriteText { @@ -79,7 +81,7 @@ namespace osu.Game.Overlays.OSD Alpha = 0.3f, Margin = new MarginPadding { Bottom = 15 }, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Text = string.IsNullOrEmpty(shortcut) ? "NO KEY BOUND" : shortcut.ToUpperInvariant() + Text = string.IsNullOrEmpty(shortcut.ToString()) ? (LocalisableString)"NO KEY BOUND" : shortcut.ToUpper() }, }; } From 7b37b1597640173a1dd51ccc958b8c034d96b7d1 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 10 Oct 2021 19:36:50 -0700 Subject: [PATCH 03/79] Localise some music action toasts --- osu.Game/Overlays/Music/MusicKeyBindingHandler.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index dba4bf926f..6fb7fb6591 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -6,9 +6,11 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Input.Bindings; +using osu.Game.Localisation; using osu.Game.Overlays.OSD; namespace osu.Game.Overlays.Music @@ -43,7 +45,7 @@ namespace osu.Game.Overlays.Music return true; case GlobalAction.MusicNext: - musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track", e.Action))); + musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast(GlobalActionKeyBindingStrings.MusicNext, e.Action))); return true; @@ -57,7 +59,7 @@ namespace osu.Game.Overlays.Music break; case PreviousTrackResult.Previous: - onScreenDisplay?.Display(new MusicActionToast("Previous track", e.Action)); + onScreenDisplay?.Display(new MusicActionToast(GlobalActionKeyBindingStrings.MusicPrev, e.Action)); break; } }); @@ -76,7 +78,7 @@ namespace osu.Game.Overlays.Music { private readonly GlobalAction action; - public MusicActionToast(string value, GlobalAction action) + public MusicActionToast(LocalisableString value, GlobalAction action) : base("Music Playback", value, string.Empty) { this.action = action; From af9bb6f27721e63f4e07398833b40b710710f585 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 10 Oct 2021 19:37:46 -0700 Subject: [PATCH 04/79] Fix padding of shortcut in toast when widest --- osu.Game/Overlays/OSD/Toast.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs index 4221bb1069..1289943123 100644 --- a/osu.Game/Overlays/OSD/Toast.cs +++ b/osu.Game/Overlays/OSD/Toast.cs @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.OSD ValueText = new OsuSpriteText { Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light), - Padding = new MarginPadding { Left = 10, Right = 10 }, + Padding = new MarginPadding { Horizontal = 10 }, Name = "Value", Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -79,7 +79,7 @@ namespace osu.Game.Overlays.OSD Origin = Anchor.BottomCentre, Name = "Shortcut", Alpha = 0.3f, - Margin = new MarginPadding { Bottom = 15 }, + Margin = new MarginPadding { Bottom = 15, Horizontal = 10 }, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), Text = string.IsNullOrEmpty(shortcut.ToString()) ? (LocalisableString)"NO KEY BOUND" : shortcut.ToUpper() }, From 4b01c23c11a6f2efd8e47ce8950104d6b7e5c49e Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 10 Oct 2021 19:55:02 -0700 Subject: [PATCH 05/79] Track ui scale setting --- osu.Game/Configuration/OsuConfigManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 05a18ef88a..9aa6372c66 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -194,7 +194,8 @@ namespace osu.Game.Configuration { string skinName = LookupSkinName(m) ?? string.Empty; return new SettingDescription(skinName, SkinSettingsStrings.SkinSectionHeader, skinName, $"{GlobalActionKeyBindingStrings.RandomSkin}: {LookupKeyBindings(GlobalAction.RandomSkin)}"); - }) + }), + new TrackedSetting(OsuSetting.UIScale, m => new SettingDescription(m, GraphicsSettingsStrings.UIScaling, $"{m.ToString("N2")}x")), // TODO: implement lookup for framework platform key bindings }; } From bae404f742cf02cd7d1c9f72d2925f73454b1613 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Oct 2021 16:17:15 +0900 Subject: [PATCH 06/79] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 5a0e7479fa..fefc2f6438 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4877ddf725..ff382f5227 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index edce9d27fe..fff0cbf418 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From 4fc84e71cdec64ce95d9c7157863fd9d3302d842 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 11 Oct 2021 01:02:26 -0700 Subject: [PATCH 07/79] Localise more toast related strings --- osu.Game/Configuration/OsuConfigManager.cs | 3 +- osu.Game/Localisation/ToastStrings.cs | 39 +++++++++++++++++++ osu.Game/OsuGame.cs | 2 +- .../Overlays/Music/MusicKeyBindingHandler.cs | 9 +++-- osu.Game/Overlays/OSD/Toast.cs | 3 +- 5 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Localisation/ToastStrings.cs diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 9aa6372c66..b763d39c3b 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -7,6 +7,7 @@ using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions; using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Input; @@ -201,7 +202,7 @@ namespace osu.Game.Configuration public Func LookupSkinName { private get; set; } - public Func LookupKeyBindings { get; set; } + public Func LookupKeyBindings { get; set; } } // IMPORTANT: These are used in user configuration files. diff --git a/osu.Game/Localisation/ToastStrings.cs b/osu.Game/Localisation/ToastStrings.cs new file mode 100644 index 0000000000..52e75425bf --- /dev/null +++ b/osu.Game/Localisation/ToastStrings.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class ToastStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.Toast"; + + /// + /// "no key bound" + /// + public static LocalisableString NoKeyBound => new TranslatableString(getKey(@"no_key_bound"), @"no key bound"); + + /// + /// "Music Playback" + /// + public static LocalisableString MusicPlayback => new TranslatableString(getKey(@"music_playback"), @"Music Playback"); + + /// + /// "Pause track" + /// + public static LocalisableString PauseTrack => new TranslatableString(getKey(@"pause_track"), @"Pause track"); + + /// + /// "Play track" + /// + public static LocalisableString PlayTrack => new TranslatableString(getKey(@"play_track"), @"Play track"); + + /// + /// "Restart track" + /// + public static LocalisableString RestartTrack => new TranslatableString(getKey(@"restart_track"), @"Restart track"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 8a018f17d9..4bf122ace1 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -638,7 +638,7 @@ namespace osu.Game var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l); if (combinations.Count == 0) - return "none"; + return ToastStrings.NoKeyBound; return string.Join(" or ", combinations); }; diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index 6fb7fb6591..18ec69e106 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; @@ -41,7 +42,7 @@ namespace osu.Game.Overlays.Music bool wasPlaying = musicController.IsPlaying; if (musicController.TogglePause()) - onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track", e.Action)); + onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? ToastStrings.PauseTrack : ToastStrings.PlayTrack, e.Action)); return true; case GlobalAction.MusicNext: @@ -55,7 +56,7 @@ namespace osu.Game.Overlays.Music switch (res) { case PreviousTrackResult.Restart: - onScreenDisplay?.Display(new MusicActionToast("Restart track", e.Action)); + onScreenDisplay?.Display(new MusicActionToast(ToastStrings.RestartTrack, e.Action)); break; case PreviousTrackResult.Previous: @@ -79,7 +80,7 @@ namespace osu.Game.Overlays.Music private readonly GlobalAction action; public MusicActionToast(LocalisableString value, GlobalAction action) - : base("Music Playback", value, string.Empty) + : base(ToastStrings.MusicPlayback, value, string.Empty) { this.action = action; } @@ -87,7 +88,7 @@ namespace osu.Game.Overlays.Music [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - ShortcutText.Text = config.LookupKeyBindings(action).ToUpperInvariant(); + ShortcutText.Text = config.LookupKeyBindings(action).ToUpper(); } } } diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs index 1289943123..12e30d8de2 100644 --- a/osu.Game/Overlays/OSD/Toast.cs +++ b/osu.Game/Overlays/OSD/Toast.cs @@ -10,6 +10,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; using osuTK.Graphics; +using osu.Game.Localisation; namespace osu.Game.Overlays.OSD { @@ -81,7 +82,7 @@ namespace osu.Game.Overlays.OSD Alpha = 0.3f, Margin = new MarginPadding { Bottom = 15, Horizontal = 10 }, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Text = string.IsNullOrEmpty(shortcut.ToString()) ? (LocalisableString)"NO KEY BOUND" : shortcut.ToUpper() + Text = string.IsNullOrEmpty(shortcut.ToString()) ? ToastStrings.NoKeyBound.ToUpper() : shortcut.ToUpper() }, }; } From e0557e849ba823273f20c13fc75e7051dc305ee1 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 11 Oct 2021 01:11:41 -0700 Subject: [PATCH 08/79] Join combinations with "/" instead --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 4bf122ace1..a3b4d90d20 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -640,7 +640,7 @@ namespace osu.Game if (combinations.Count == 0) return ToastStrings.NoKeyBound; - return string.Join(" or ", combinations); + return string.Join(" / ", combinations); }; Container logoContainer; From 205d95e8c62ce26cf6c6ebe63c01edb7e9b9b9f9 Mon Sep 17 00:00:00 2001 From: StanR Date: Wed, 13 Oct 2021 20:04:34 +0300 Subject: [PATCH 09/79] Approximate amount of effective misses using combo --- .../Difficulty/OsuDifficultyAttributes.cs | 1 + .../Difficulty/OsuDifficultyCalculator.cs | 2 + .../Difficulty/OsuPerformanceCalculator.cs | 38 ++++++++++++++----- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index bd4c0f2ad5..a08fe3b7c5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty public double OverallDifficulty { get; set; } public double DrainRate { get; set; } public int HitCircleCount { get; set; } + public int SliderCount { get; set; } public int SpinnerCount { get; set; } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 790aa0eb7d..b0a764dc4d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -64,6 +64,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty maxCombo += beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1); int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); + int sliderCount = beatmap.HitObjects.Count(h => h is Slider); int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner); return new OsuDifficultyAttributes @@ -78,6 +79,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty DrainRate = drainRate, MaxCombo = maxCombo, HitCircleCount = hitCirclesCount, + SliderCount = sliderCount, SpinnerCount = spinnerCount, Skills = skills }; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 4e4dbc02a1..d0b5e877b5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int countMeh; private int countMiss; + private int effectiveMissCount; + public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) : base(ruleset, attributes, score) { @@ -39,19 +41,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); + effectiveMissCount = calculateEffectiveMissCount(); double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. // Custom multipliers for NoFail and SpunOut. if (mods.Any(m => m is OsuModNoFail)) - multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss); + multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount); if (mods.Any(m => m is OsuModSpunOut)) multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); if (mods.Any(h => h is OsuModRelax)) { - countMiss += countOk + countMeh; + effectiveMissCount += countOk + countMeh; multiplier *= 0.6; } @@ -97,8 +100,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= lengthBonus; // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. - if (countMiss > 0) - aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss); + if (effectiveMissCount > 0) + aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount); // Combo scaling. if (Attributes.MaxCombo > 0) @@ -115,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; if (mods.Any(m => m is OsuModBlinds)) - aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * countMiss)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); + aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); else if (mods.Any(h => h is OsuModHidden)) { // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. @@ -142,8 +145,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= lengthBonus; // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. - if (countMiss > 0) - speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875)); + if (effectiveMissCount > 0) + speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); // Combo scaling. if (Attributes.MaxCombo > 0) @@ -231,8 +234,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty flashlightValue *= 1.3; // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. - if (countMiss > 0) - flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875)); + if (effectiveMissCount > 0) + flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); // Combo scaling. if (Attributes.MaxCombo > 0) @@ -250,6 +253,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty return flashlightValue; } + private int calculateEffectiveMissCount() + { + // guess the number of misses + slider breaks from combo + double comboBasedMissCount = 0.0; + + if (Attributes.SliderCount > 0) + { + double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount; + if (scoreMaxCombo < fullComboThreshold) + comboBasedMissCount = fullComboThreshold / scoreMaxCombo; + else + comboBasedMissCount = Math.Pow((Attributes.MaxCombo - scoreMaxCombo) / (0.1 * Attributes.SliderCount), 3); + } + + return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount)); + } + private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; } From d95a62fa563ae10ef8453bff223f88dd5418616a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 23:45:09 +0900 Subject: [PATCH 10/79] Add models and stores for beatmap manager requirements --- osu.Game/Database/RealmContextFactory.cs | 23 + osu.Game/Stores/ArchiveModelImporter.cs | 557 +++++++++++++++++++++++ osu.Game/Stores/BeatmapImporter.cs | 336 ++++++++++++++ 3 files changed, 916 insertions(+) create mode 100644 osu.Game/Stores/ArchiveModelImporter.cs create mode 100644 osu.Game/Stores/BeatmapImporter.cs diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 82d51e365e..933454f860 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; +using osu.Game.Models; using Realms; #nullable enable @@ -70,6 +72,27 @@ namespace osu.Game.Database if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) Filename += realm_extension; + + cleanupPendingDeletions(); + } + + private void cleanupPendingDeletions() + { + using (var realm = CreateContext()) + using (var transaction = realm.BeginWrite()) + { + var pendingDeleteSets = realm.All().Where(s => s.DeletePending); + + foreach (var s in pendingDeleteSets) + { + foreach (var b in s.Beatmaps) + realm.Remove(b); + + realm.Remove(s); + } + + transaction.Commit(); + } } /// diff --git a/osu.Game/Stores/ArchiveModelImporter.cs b/osu.Game/Stores/ArchiveModelImporter.cs new file mode 100644 index 0000000000..c165dbecd8 --- /dev/null +++ b/osu.Game/Stores/ArchiveModelImporter.cs @@ -0,0 +1,557 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Humanizer; +using NuGet.Packaging; +using osu.Framework.Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Threading; +using osu.Game.Database; +using osu.Game.IO.Archives; +using osu.Game.Models; +using osu.Game.Overlays.Notifications; +using Realms; + +#nullable enable + +namespace osu.Game.Stores +{ + /// + /// Encapsulates a model store class to give it import functionality. + /// Adds cross-functionality with to give access to the central file store for the provided model. + /// + /// The model type. + public abstract class ArchiveModelImporter : IModelImporter + where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete + { + private const int import_queue_request_concurrency = 1; + + /// + /// The size of a batch import operation before considering it a lower priority operation. + /// + private const int low_priority_import_batch_size = 1; + + /// + /// A singleton scheduler shared by all . + /// + /// + /// This scheduler generally performs IO and CPU intensive work so concurrency is limited harshly. + /// It is mainly being used as a queue mechanism for large imports. + /// + private static readonly ThreadedTaskScheduler import_scheduler = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelImporter)); + + /// + /// A second scheduler for lower priority imports. + /// For simplicity, these will just run in parallel with normal priority imports, but a future refactor would see this implemented via a custom scheduler/queue. + /// See https://gist.github.com/peppy/f0e118a14751fc832ca30dd48ba3876b for an incomplete version of this. + /// + private static readonly ThreadedTaskScheduler import_scheduler_low_priority = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelImporter)); + + public virtual IEnumerable HandledExtensions => new[] { @".zip" }; + + protected readonly RealmFileStore Files; + + protected readonly RealmContextFactory ContextFactory; + + /// + /// Fired when the user requests to view the resulting import. + /// + public Action>>? PresentImport; + + /// + /// Set an endpoint for notifications to be posted to. + /// + public Action? PostNotification { protected get; set; } + + protected ArchiveModelImporter(Storage storage, RealmContextFactory contextFactory) + { + ContextFactory = contextFactory; + + Files = new RealmFileStore(contextFactory, storage); + } + + /// + /// Import one or more items from filesystem . + /// + /// + /// This will be treated as a low priority import if more than one path is specified; use to always import at standard priority. + /// This will post notifications tracking progress. + /// + /// One or more archive locations on disk. + public Task Import(params string[] paths) + { + var notification = new ProgressNotification { State = ProgressNotificationState.Active }; + + PostNotification?.Invoke(notification); + + return Import(notification, paths.Select(p => new ImportTask(p)).ToArray()); + } + + public Task Import(params ImportTask[] tasks) + { + var notification = new ProgressNotification { State = ProgressNotificationState.Active }; + + PostNotification?.Invoke(notification); + + return Import(notification, tasks); + } + + public async Task>> Import(ProgressNotification notification, params ImportTask[] tasks) + { + if (tasks.Length == 0) + { + notification.CompletionText = $"No {HumanisedModelName}s were found to import!"; + notification.State = ProgressNotificationState.Completed; + return Enumerable.Empty>(); + } + + notification.Progress = 0; + notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising..."; + + int current = 0; + + var imported = new List>(); + + bool isLowPriorityImport = tasks.Length > low_priority_import_batch_size; + + try + { + await Task.WhenAll(tasks.Select(async task => + { + notification.CancellationToken.ThrowIfCancellationRequested(); + + try + { + var model = await Import(task, isLowPriorityImport, notification.CancellationToken).ConfigureAwait(false); + + lock (imported) + { + if (model != null) + imported.Add(model); + current++; + + notification.Text = $"Imported {current} of {tasks.Length} {HumanisedModelName}s"; + notification.Progress = (float)current / tasks.Length; + } + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception e) + { + Logger.Error(e, $@"Could not import ({task})", LoggingTarget.Database); + } + })).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + if (imported.Count == 0) + { + notification.State = ProgressNotificationState.Cancelled; + return imported; + } + } + + if (imported.Count == 0) + { + notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import failed!"; + notification.State = ProgressNotificationState.Cancelled; + } + else + { + notification.CompletionText = imported.Count == 1 + ? $"Imported {imported.First()}!" + : $"Imported {imported.Count} {HumanisedModelName}s!"; + + if (imported.Count > 0 && PresentImport != null) + { + notification.CompletionText += " Click to view."; + notification.CompletionClickAction = () => + { + PresentImport?.Invoke(imported); + return true; + }; + } + + notification.State = ProgressNotificationState.Completed; + } + + return imported; + } + + /// + /// Import one from the filesystem and delete the file on success. + /// Note that this bypasses the UI flow and should only be used for special cases or testing. + /// + /// The containing data about the to import. + /// Whether this is a low priority import. + /// An optional cancellation token. + /// The imported model, if successful. + public async Task?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + ILive? import; + using (ArchiveReader reader = task.GetReader()) + import = await Import(reader, lowPriority, cancellationToken).ConfigureAwait(false); + + // We may or may not want to delete the file depending on where it is stored. + // e.g. reconstructing/repairing database with items from default storage. + // Also, not always a single file, i.e. for LegacyFilesystemReader + // TODO: Add a check to prevent files from storage to be deleted. + try + { + if (import != null && File.Exists(task.Path) && ShouldDeleteArchive(task.Path)) + File.Delete(task.Path); + } + catch (Exception e) + { + Logger.Error(e, $@"Could not delete original file after import ({task})"); + } + + return import; + } + + /// + /// Silently import an item from an . + /// + /// The archive to be imported. + /// Whether this is a low priority import. + /// An optional cancellation token. + public async Task?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + TModel? model = null; + + try + { + model = CreateModel(archive); + + if (model == null) + return null; + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception e) + { + LogForModel(model, @$"Model creation of {archive.Name} failed.", e); + return null; + } + + var scheduledImport = Task.Factory.StartNew(async () => await Import(model, archive, lowPriority, cancellationToken).ConfigureAwait(false), + cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap(); + + return await scheduledImport.ConfigureAwait(true); + } + + /// + /// Any file extensions which should be included in hash creation. + /// Generally should include all file types which determine the file's uniqueness. + /// Large files should be avoided if possible. + /// + /// + /// This is only used by the default hash implementation. If is overridden, it will not be used. + /// + protected abstract string[] HashableFileTypes { get; } + + internal static void LogForModel(TModel? model, string message, Exception? e = null) + { + string trimmedHash; + if (model == null || !model.IsValid || string.IsNullOrEmpty(model.Hash)) + trimmedHash = "?????"; + else + trimmedHash = model.Hash.Substring(0, 5); + + string prefix = $"[{trimmedHash}]"; + + if (e != null) + Logger.Error(e, $"{prefix} {message}", LoggingTarget.Database); + else + Logger.Log($"{prefix} {message}", LoggingTarget.Database); + } + + /// + /// Whether the implementation overrides with a custom implementation. + /// Custom hash implementations must bypass the early exit in the import flow (see usage). + /// + protected virtual bool HasCustomHashFunction => false; + + /// + /// Create a SHA-2 hash from the provided archive based on file content of all files matching . + /// + /// + /// In the case of no matching files, a hash will be generated from the passed archive's . + /// + protected virtual string ComputeHash(TModel item, ArchiveReader? reader = null) + { + if (reader != null) + // fast hashing for cases where the item's files may not be populated. + return computeHashFast(reader); + + // for now, concatenate all hashable files in the set to create a unique hash. + MemoryStream hashable = new MemoryStream(); + + foreach (RealmNamedFileUsage file in item.Files.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f.Filename)) + { + using (Stream s = Files.Store.GetStream(file.File.StoragePath)) + s.CopyTo(hashable); + } + + if (hashable.Length > 0) + return hashable.ComputeSHA2Hash(); + + return item.Hash; + } + + /// + /// Silently import an item from a . + /// + /// The model to be imported. + /// An optional archive to use for model population. + /// Whether this is a low priority import. + /// An optional cancellation token. + public virtual async Task?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + { + using (var realm = ContextFactory.CreateContext()) + { + cancellationToken.ThrowIfCancellationRequested(); + + bool checkedExisting = false; + TModel? existing = null; + + if (archive != null && !HasCustomHashFunction) + { + // this is a fast bail condition to improve large import performance. + item.Hash = computeHashFast(archive); + + checkedExisting = true; + existing = CheckForExisting(item, realm); + + if (existing != null) + { + // bare minimum comparisons + // + // note that this should really be checking filesizes on disk (of existing files) for some degree of sanity. + // or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files. + if (CanSkipImport(existing, item) && + getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f))) + { + LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); + + using (var transaction = realm.BeginWrite()) + { + existing.DeletePending = false; + transaction.Commit(); + } + + return existing.ToLive(); + } + + LogForModel(item, @"Found existing (optimised) but failed pre-check."); + } + } + + try + { + LogForModel(item, @"Beginning import..."); + + // TODO: do we want to make the transaction this local? not 100% sure, will need further investigation. + using (var transaction = realm.BeginWrite()) + { + if (archive != null) + // TODO: look into rollback of file additions (or delayed commit). + item.Files.AddRange(createFileInfos(archive, Files, realm)); + + item.Hash = ComputeHash(item, archive); + + // TODO: we may want to run this outside of the transaction. + await Populate(item, archive, realm, cancellationToken).ConfigureAwait(false); + + if (!checkedExisting) + existing = CheckForExisting(item, realm); + + if (existing != null) + { + if (CanReuseExisting(existing, item)) + { + LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); + existing.DeletePending = false; + + return existing.ToLive(); + } + + LogForModel(item, @"Found existing but failed re-use check."); + + existing.DeletePending = true; + + // todo: actually delete? i don't think this is required... + // ModelStore.PurgeDeletable(s => s.ID == existing.ID); + } + + PreImport(item, realm); + + // import to store + realm.Add(item); + + transaction.Commit(); + } + + LogForModel(item, @"Import successfully completed!"); + } + catch (Exception e) + { + if (!(e is TaskCanceledException)) + LogForModel(item, @"Database import or population failed and has been rolled back.", e); + + throw; + } + + return item.ToLive(); + } + } + + private string computeHashFast(ArchiveReader reader) + { + MemoryStream hashable = new MemoryStream(); + + foreach (var file in reader.Filenames.Where(f => HashableFileTypes.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f)) + { + using (Stream s = reader.GetStream(file)) + s.CopyTo(hashable); + } + + if (hashable.Length > 0) + return hashable.ComputeSHA2Hash(); + + return reader.Name.ComputeSHA2Hash(); + } + + /// + /// Create all required s for the provided archive, adding them to the global file store. + /// + private List createFileInfos(ArchiveReader reader, RealmFileStore files, Realm realm) + { + var fileInfos = new List(); + + // import files to manager + foreach (var filenames in getShortenedFilenames(reader)) + { + using (Stream s = reader.GetStream(filenames.original)) + { + var item = new RealmNamedFileUsage(files.Add(s, realm), filenames.shortened); + fileInfos.Add(item); + } + } + + return fileInfos; + } + + private IEnumerable<(string original, string shortened)> getShortenedFilenames(ArchiveReader reader) + { + string prefix = reader.Filenames.GetCommonPrefix(); + if (!(prefix.EndsWith('/') || prefix.EndsWith('\\'))) + prefix = string.Empty; + + // import files to manager + foreach (string file in reader.Filenames) + yield return (file, file.Substring(prefix.Length).ToStandardisedPath()); + } + + /// + /// Create a barebones model from the provided archive. + /// Actual expensive population should be done in ; this should just prepare for duplicate checking. + /// + /// The archive to create the model for. + /// A model populated with minimal information. Returning a null will abort importing silently. + protected abstract TModel? CreateModel(ArchiveReader archive); + + /// + /// Populate the provided model completely from the given archive. + /// After this method, the model should be in a state ready to commit to a store. + /// + /// The model to populate. + /// The archive to use as a reference for population. May be null. + /// The current realm context. + /// An optional cancellation token. + protected abstract Task Populate(TModel model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default); + + /// + /// Perform any final actions before the import to database executes. + /// + /// The model prepared for import. + /// The current realm context. + protected virtual void PreImport(TModel model, Realm realm) + { + } + + /// + /// Check whether an existing model already exists for a new import item. + /// + /// The new model proposed for import. + /// The current realm context. + /// An existing model which matches the criteria to skip importing, else null. + protected TModel? CheckForExisting(TModel model, Realm realm) => string.IsNullOrEmpty(model.Hash) ? null : realm.All().FirstOrDefault(b => b.Hash == model.Hash); + + /// + /// Whether import can be skipped after finding an existing import early in the process. + /// Only valid when is not overridden. + /// + /// The existing model. + /// The newly imported model. + /// Whether to skip this import completely. + protected virtual bool CanSkipImport(TModel existing, TModel import) => true; + + /// + /// After an existing is found during an import process, the default behaviour is to use/restore the existing + /// item and skip the import. This method allows changing that behaviour. + /// + /// The existing model. + /// The newly imported model. + /// Whether the existing model should be restored and used. Returning false will delete the existing and force a re-import. + protected virtual bool CanReuseExisting(TModel existing, TModel import) => + // for the best or worst, we copy and import files of a new import before checking whether + // it is a duplicate. so to check if anything has changed, we can just compare all File IDs. + getIDs(existing.Files).SequenceEqual(getIDs(import.Files)) && + getFilenames(existing.Files).SequenceEqual(getFilenames(import.Files)); + + /// + /// Whether this specified path should be removed after successful import. + /// + /// The path for consideration. May be a file or a directory. + /// Whether to perform deletion. + protected virtual bool ShouldDeleteArchive(string path) => false; + + private IEnumerable getIDs(IEnumerable files) + { + foreach (var f in files.OrderBy(f => f.Filename)) + yield return f.File.Hash; + } + + private IEnumerable getFilenames(IEnumerable files) + { + foreach (var f in files.OrderBy(f => f.Filename)) + yield return f.Filename; + } + + public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; + + private string getValidFilename(string filename) + { + foreach (char c in Path.GetInvalidFileNameChars()) + filename = filename.Replace(c, '_'); + return filename; + } + } +} diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs new file mode 100644 index 0000000000..95d2c4fe7b --- /dev/null +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -0,0 +1,336 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NuGet.Packaging; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics.Textures; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.Database; +using osu.Game.IO; +using osu.Game.IO.Archives; +using osu.Game.Models; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Skinning; +using Realms; + +#nullable enable + +namespace osu.Game.Stores +{ + /// + /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. + /// + [ExcludeFromDynamicCompile] + public class BeatmapImporter : ArchiveModelImporter, IDisposable + { + public override IEnumerable HandledExtensions => new[] { ".osz" }; + + protected override string[] HashableFileTypes => new[] { ".osu" }; + + // protected override string ImportFromStablePath => "."; + // + // protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); + // + // protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; + // + // protected override bool CheckLocalAvailability(RealmBeatmapSet model, System.Linq.IQueryable items) + // => base.CheckLocalAvailability(model, items) || (model.OnlineID != null && items.Any(b => b.OnlineID == model.OnlineID)); + + private readonly dynamic? onlineLookupQueue = null; // todo: BeatmapOnlineLookupQueue is private + + public BeatmapImporter(RealmContextFactory contextFactory, Storage storage, bool performOnlineLookups = false) + : base(storage, contextFactory) + { + if (performOnlineLookups) + { + // onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); + } + } + + protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz"; + + protected override async Task Populate(RealmBeatmapSet beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) + { + if (archive != null) + beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet.Files, realm)); + + foreach (RealmBeatmap b in beatmapSet.Beatmaps) + b.BeatmapSet = beatmapSet; + + validateOnlineIds(beatmapSet, realm); + + bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineID > 0); + + if (onlineLookupQueue != null) + await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); + + // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. + if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineID > 0)) + { + if (beatmapSet.OnlineID != null) + { + beatmapSet.OnlineID = null; + LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); + } + } + } + + protected override void PreImport(RealmBeatmapSet beatmapSet, Realm realm) + { + // if (beatmapSet.Beatmaps.Any(b => b.Difficulty == null)) + // throw new InvalidOperationException($"Cannot import {nameof(IBeatmapInfo)} with null {nameof(IBeatmapInfo.Difficulty)}."); + + // check if a set already exists with the same online id, delete if it does. + if (beatmapSet.OnlineID != null) + { + var existingOnlineId = realm.All().FirstOrDefault(b => b.OnlineID == beatmapSet.OnlineID); + + if (existingOnlineId != null) + { + existingOnlineId.DeletePending = true; + + // in order to avoid a unique key constraint, immediately remove the online ID from the previous set. + existingOnlineId.OnlineID = null; + foreach (var b in existingOnlineId.Beatmaps) + b.OnlineID = null; + + LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It has been deleted."); + } + } + } + + private void validateOnlineIds(RealmBeatmapSet beatmapSet, Realm realm) + { + var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineID.HasValue).Select(b => b.OnlineID).ToList(); + + // ensure all IDs are unique + if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1)) + { + LogForModel(beatmapSet, "Found non-unique IDs, resetting..."); + resetIds(); + return; + } + + // find any existing beatmaps in the database that have matching online ids + List existingBeatmaps = new List(); + + foreach (var id in beatmapIds) + existingBeatmaps.AddRange(realm.All().Where(b => b.OnlineID == id)); + + if (existingBeatmaps.Any()) + { + // reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set. + // we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted. + + var existing = CheckForExisting(beatmapSet, realm); + + if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b))) + { + LogForModel(beatmapSet, "Found existing import with online IDs already, resetting..."); + resetIds(); + } + } + + void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineID = null); + } + + protected override bool CanSkipImport(RealmBeatmapSet existing, RealmBeatmapSet import) + { + if (!base.CanSkipImport(existing, import)) + return false; + + return existing.Beatmaps.Any(b => b.OnlineID != null); + } + + protected override bool CanReuseExisting(RealmBeatmapSet existing, RealmBeatmapSet import) + { + if (!base.CanReuseExisting(existing, import)) + return false; + + var existingIds = existing.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i); + var importIds = import.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i); + + // force re-import if we are not in a sane state. + return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds); + } + + public override string HumanisedModelName => "beatmap"; + + protected override RealmBeatmapSet? CreateModel(ArchiveReader reader) + { + // let's make sure there are actually .osu files to import. + string? mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)); + + if (string.IsNullOrEmpty(mapName)) + { + Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database); + return null; + } + + Beatmap beatmap; + using (var stream = new LineBufferedReader(reader.GetStream(mapName))) + beatmap = Decoder.GetDecoder(stream).Decode(stream); + + return new RealmBeatmapSet + { + OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID, + // Metadata = beatmap.Metadata, + DateAdded = DateTimeOffset.UtcNow + }; + } + + /// + /// Create all required s for the provided archive. + /// + private List createBeatmapDifficulties(IList files, Realm realm) + { + var beatmaps = new List(); + + foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) + { + using (var raw = Files.Store.GetStream(file.File.StoragePath)) + using (var ms = new MemoryStream()) // we need a memory stream so we can seek + using (var sr = new LineBufferedReader(ms)) + { + raw.CopyTo(ms); + ms.Position = 0; + + var decoder = Decoder.GetDecoder(sr); + IBeatmap beatmap = decoder.Decode(sr); + + string hash = ms.ComputeSHA2Hash(); + + if (beatmaps.Any(b => b.Hash == hash)) + continue; + + var beatmapInfo = beatmap.BeatmapInfo; + + var ruleset = realm.All().FirstOrDefault(r => r.OnlineID == beatmapInfo.RulesetID); + var rulesetInstance = (ruleset as IRulesetInfo)?.CreateInstance(); + + if (ruleset == null || rulesetInstance == null) + { + Logger.Log($"Skipping import due to missing local ruleset {beatmapInfo.RulesetID}.", LoggingTarget.Database); + continue; + } + + beatmapInfo.Path = file.Filename; + beatmapInfo.Hash = hash; + beatmapInfo.MD5Hash = ms.ComputeMD5Hash(); + + // TODO: this should be done in a better place once we actually need to dynamically update it. + beatmap.BeatmapInfo.Ruleset = rulesetInstance.RulesetInfo; + beatmap.BeatmapInfo.StarDifficulty = rulesetInstance.CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating; + beatmap.BeatmapInfo.Length = calculateLength(beatmap); + beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); + + var difficulty = new RealmBeatmapDifficulty + { + DrainRate = beatmapInfo.BaseDifficulty.DrainRate, + CircleSize = beatmapInfo.BaseDifficulty.CircleSize, + OverallDifficulty = beatmapInfo.BaseDifficulty.OverallDifficulty, + ApproachRate = beatmapInfo.BaseDifficulty.ApproachRate, + SliderMultiplier = beatmapInfo.BaseDifficulty.SliderMultiplier, + SliderTickRate = beatmapInfo.BaseDifficulty.SliderTickRate, + }; + + var metadata = new RealmBeatmapMetadata + { + Title = beatmap.Metadata.Title, + TitleUnicode = beatmap.Metadata.TitleUnicode, + Artist = beatmap.Metadata.Artist, + ArtistUnicode = beatmap.Metadata.ArtistUnicode, + Author = beatmap.Metadata.AuthorString, + Source = beatmap.Metadata.Source, + Tags = beatmap.Metadata.Tags, + PreviewTime = beatmap.Metadata.PreviewTime, + AudioFile = beatmap.Metadata.AudioFile, + BackgroundFile = beatmap.Metadata.BackgroundFile, + }; + + var realmBeatmap = new RealmBeatmap(ruleset, difficulty, metadata) + { + DifficultyName = beatmapInfo.Version, + OnlineID = beatmapInfo.OnlineBeatmapID, + Length = beatmapInfo.Length, + BPM = beatmapInfo.BPM, + Hash = beatmapInfo.Hash, + StarRating = beatmapInfo.StarDifficulty, + MD5Hash = beatmapInfo.MD5Hash, + Hidden = beatmapInfo.Hidden, + AudioLeadIn = beatmapInfo.AudioLeadIn, + StackLeniency = beatmapInfo.StackLeniency, + SpecialStyle = beatmapInfo.SpecialStyle, + LetterboxInBreaks = beatmapInfo.LetterboxInBreaks, + WidescreenStoryboard = beatmapInfo.WidescreenStoryboard, + EpilepsyWarning = beatmapInfo.EpilepsyWarning, + SamplesMatchPlaybackRate = beatmapInfo.SamplesMatchPlaybackRate, + DistanceSpacing = beatmapInfo.DistanceSpacing, + BeatDivisor = beatmapInfo.BeatDivisor, + GridSize = beatmapInfo.GridSize, + TimelineZoom = beatmapInfo.TimelineZoom, + }; + + // TODO: IBeatmap.BeatmapInfo needs to be updated to the new interface. + // beatmaps.Add(beatmap.BeatmapInfo); + + beatmaps.Add(realmBeatmap); + } + } + + return beatmaps; + } + + public void Dispose() + { + onlineLookupQueue?.Dispose(); + } + + private double calculateLength(IBeatmap b) + { + if (!b.HitObjects.Any()) + return 0; + + var lastObject = b.HitObjects.Last(); + + //TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list). + double endTime = lastObject.GetEndTime(); + double startTime = b.HitObjects.First().StartTime; + + return endTime - startTime; + } + + /// + /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. + /// + private class DummyConversionBeatmap : WorkingBeatmap + { + private readonly IBeatmap beatmap; + + public DummyConversionBeatmap(IBeatmap beatmap) + : base(beatmap.BeatmapInfo, null) + { + this.beatmap = beatmap; + } + + protected override IBeatmap GetBeatmap() => beatmap; + protected override Texture? GetBackground() => null; + protected override Track? GetBeatmapTrack() => null; + protected internal override ISkin? GetSkin() => null; + public override Stream? GetStream(string storagePath) => null; + } + } +} From 49969ac32850f44cc3c1b1ec903939ebfb0cb18c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 00:05:14 +0900 Subject: [PATCH 11/79] Add beatmap import and file store tests --- .../Database/BeatmapImporterTests.cs | 820 ++++++++++++++++++ osu.Game/Database/IPostImports.cs | 4 +- osu.Game/Stores/ArchiveModelImporter.cs | 6 +- 3 files changed, 826 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tests/Database/BeatmapImporterTests.cs diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs new file mode 100644 index 0000000000..beab9b311b --- /dev/null +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -0,0 +1,820 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Logging; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.IO.Archives; +using osu.Game.Models; +using osu.Game.Stores; +using osu.Game.Tests.Resources; +using Realms; +using SharpCompress.Archives; +using SharpCompress.Archives.Zip; +using SharpCompress.Common; +using SharpCompress.Writers.Zip; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + [TestFixture] + public class BeatmapImporterTests : RealmTest + { + [Test] + public void TestImportBeatmapThenCleanup() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using (var importer = new BeatmapImporter(realmFactory, storage)) + using (new RealmRulesetStore(realmFactory, storage)) + { + ILive? imported; + + using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) + imported = await importer.Import(reader); + + Assert.AreEqual(1, realmFactory.Context.All().Count()); + + Assert.NotNull(imported); + Debug.Assert(imported != null); + + imported.PerformWrite(s => s.DeletePending = true); + + Assert.AreEqual(1, realmFactory.Context.All().Count(s => s.DeletePending)); + } + }); + + Logger.Log("Running with no work to purge pending deletions"); + + RunTestWithRealm((realmFactory, _) => { Assert.AreEqual(0, realmFactory.Context.All().Count()); }); + } + + [Test] + public void TestImportWhenClosed() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + await LoadOszIntoStore(importer, realmFactory.Context); + }); + } + + [Test] + public void TestImportThenDelete() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var imported = await LoadOszIntoStore(importer, realmFactory.Context); + + deleteBeatmapSet(imported, realmFactory.Context); + }); + } + + [Test] + public void TestImportThenDeleteFromStream() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var tempPath = TestResources.GetTestBeatmapForImport(); + + ILive? importedSet; + + using (var stream = File.OpenRead(tempPath)) + { + importedSet = await importer.Import(new ImportTask(stream, Path.GetFileName(tempPath))); + ensureLoaded(realmFactory.Context); + } + + Assert.NotNull(importedSet); + Debug.Assert(importedSet != null); + + Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing"); + File.Delete(tempPath); + + var imported = realmFactory.Context.All().First(beatmapSet => beatmapSet.ID == importedSet.ID); + + deleteBeatmapSet(imported, realmFactory.Context); + }); + } + + [Test] + public void TestImportThenImport() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.ID == importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + + checkBeatmapSetCount(realmFactory.Context, 1); + checkSingleReferencedFileCount(realmFactory.Context, 18); + }); + } + + [Test] + public void TestImportThenImportWithReZip() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + var imported = await LoadOszIntoStore(importer, realmFactory.Context); + + string hashBefore = hashFile(temp); + + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + // zip files differ because different compression or encoder. + Assert.AreNotEqual(hashBefore, hashFile(temp)); + + var importedSecondTime = await importer.Import(new ImportTask(temp)); + + ensureLoaded(realmFactory.Context); + + Assert.NotNull(importedSecondTime); + Debug.Assert(importedSecondTime != null); + + // but contents doesn't, so existing should still be used. + Assert.IsTrue(imported.ID == importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.PerformRead(s => s.Beatmaps.First().ID)); + } + finally + { + Directory.Delete(extractedFolder, true); + } + }); + } + + [Test] + public void TestImportThenImportWithChangedHashedFile() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + var imported = await LoadOszIntoStore(importer, realmFactory.Context); + + await createScoreForBeatmap(realmFactory.Context, imported.Beatmaps.First()); + + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + // arbitrary write to hashed file + // this triggers the special BeatmapManager.PreImport deletion/replacement flow. + using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).AppendText()) + await sw.WriteLineAsync("// changed"); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + var importedSecondTime = await importer.Import(new ImportTask(temp)); + + ensureLoaded(realmFactory.Context); + + // check the newly "imported" beatmap is not the original. + Assert.NotNull(importedSecondTime); + Debug.Assert(importedSecondTime != null); + + Assert.IsTrue(imported.ID != importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID)); + } + finally + { + Directory.Delete(extractedFolder, true); + } + }); + } + + [Test] + [Ignore("intentionally broken by import optimisations")] + public void TestImportThenImportWithChangedFile() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + var imported = await LoadOszIntoStore(importer, realmFactory.Context); + + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + // arbitrary write to non-hashed file + using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.mp3").First()).AppendText()) + await sw.WriteLineAsync("text"); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + var importedSecondTime = await importer.Import(new ImportTask(temp)); + + ensureLoaded(realmFactory.Context); + + Assert.NotNull(importedSecondTime); + Debug.Assert(importedSecondTime != null); + + // check the newly "imported" beatmap is not the original. + Assert.IsTrue(imported.ID != importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID)); + } + finally + { + Directory.Delete(extractedFolder, true); + } + }); + } + + [Test] + public void TestImportThenImportWithDifferentFilename() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + var imported = await LoadOszIntoStore(importer, realmFactory.Context); + + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + // change filename + var firstFile = new FileInfo(Directory.GetFiles(extractedFolder).First()); + firstFile.MoveTo(Path.Combine(firstFile.DirectoryName.AsNonNull(), $"{firstFile.Name}-changed{firstFile.Extension}")); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + var importedSecondTime = await importer.Import(new ImportTask(temp)); + + ensureLoaded(realmFactory.Context); + + Assert.NotNull(importedSecondTime); + Debug.Assert(importedSecondTime != null); + + // check the newly "imported" beatmap is not the original. + Assert.IsTrue(imported.ID != importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID)); + } + finally + { + Directory.Delete(extractedFolder, true); + } + }); + } + + [Test] + [Ignore("intentionally broken by import optimisations")] + public void TestImportCorruptThenImport() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var imported = await LoadOszIntoStore(importer, realmFactory.Context); + + var firstFile = imported.Files.First(); + + long originalLength; + using (var stream = storage.GetStream(firstFile.File.StoragePath)) + originalLength = stream.Length; + + using (var stream = storage.GetStream(firstFile.File.StoragePath, FileAccess.Write, FileMode.Create)) + stream.WriteByte(0); + + var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + + using (var stream = storage.GetStream(firstFile.File.StoragePath)) + Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import"); + + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.ID == importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + + checkBeatmapSetCount(realmFactory.Context, 1); + checkSingleReferencedFileCount(realmFactory.Context, 18); + }); + } + + [Test] + public void TestRollbackOnFailure() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + int loggedExceptionCount = 0; + + Logger.NewEntry += l => + { + if (l.Target == LoggingTarget.Database && l.Exception != null) + Interlocked.Increment(ref loggedExceptionCount); + }; + + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var imported = await LoadOszIntoStore(importer, realmFactory.Context); + + realmFactory.Context.Write(() => imported.Hash += "-changed"); + + checkBeatmapSetCount(realmFactory.Context, 1); + checkBeatmapCount(realmFactory.Context, 12); + checkSingleReferencedFileCount(realmFactory.Context, 18); + + var brokenTempFilename = TestResources.GetTestBeatmapForImport(); + + MemoryStream brokenOsu = new MemoryStream(); + MemoryStream brokenOsz = new MemoryStream(await File.ReadAllBytesAsync(brokenTempFilename)); + + File.Delete(brokenTempFilename); + + using (var outStream = File.Open(brokenTempFilename, FileMode.CreateNew)) + using (var zip = ZipArchive.Open(brokenOsz)) + { + zip.AddEntry("broken.osu", brokenOsu, false); + zip.SaveTo(outStream, CompressionType.Deflate); + } + + // this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu. + try + { + await importer.Import(new ImportTask(brokenTempFilename)); + } + catch + { + } + + checkBeatmapSetCount(realmFactory.Context, 1); + checkBeatmapCount(realmFactory.Context, 12); + + checkSingleReferencedFileCount(realmFactory.Context, 18); + + Assert.AreEqual(1, loggedExceptionCount); + + File.Delete(brokenTempFilename); + }); + } + + [Test] + public void TestImportThenDeleteThenImport() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var imported = await LoadOszIntoStore(importer, realmFactory.Context); + + deleteBeatmapSet(imported, realmFactory.Context); + + var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.ID == importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + }); + } + + [Test] + public void TestImportThenDeleteThenImportWithOnlineIDsMissing() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var imported = await LoadOszIntoStore(importer, realmFactory.Context); + + realmFactory.Context.Write(() => + { + foreach (var b in imported.Beatmaps) + b.OnlineID = null; + }); + + deleteBeatmapSet(imported, realmFactory.Context); + + var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + + // check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched) + Assert.IsTrue(imported.ID != importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); + }); + } + + [Test] + public void TestImportWithDuplicateBeatmapIDs() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var metadata = new RealmBeatmapMetadata + { + Artist = "SomeArtist", + Author = "SomeAuthor" + }; + + var ruleset = realmFactory.Context.All().First(); + + var toImport = new RealmBeatmapSet + { + OnlineID = 1, + Beatmaps = + { + new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) + { + OnlineID = 2, + }, + new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) + { + OnlineID = 2, + Status = BeatmapSetOnlineStatus.Loved, + } + } + }; + + var imported = await importer.Import(toImport); + + Assert.NotNull(imported); + Debug.Assert(imported != null); + + Assert.AreEqual(null, imported.PerformRead(s => s.Beatmaps[0].OnlineID)); + Assert.AreEqual(null, imported.PerformRead(s => s.Beatmaps[1].OnlineID)); + }); + } + + [Test] + public void TestImportWhenFileOpen() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var temp = TestResources.GetTestBeatmapForImport(); + using (File.OpenRead(temp)) + await importer.Import(temp); + ensureLoaded(realmFactory.Context); + File.Delete(temp); + Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); + }); + } + + [Test] + public void TestImportWithDuplicateHashes() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.AddEntry("duplicate.osu", Directory.GetFiles(extractedFolder, "*.osu").First()); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + await importer.Import(temp); + + ensureLoaded(realmFactory.Context); + } + finally + { + Directory.Delete(extractedFolder, true); + } + }); + } + + [Test] + public void TestImportNestedStructure() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + string subfolder = Path.Combine(extractedFolder, "subfolder"); + + Directory.CreateDirectory(subfolder); + + try + { + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(subfolder); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + var imported = await importer.Import(new ImportTask(temp)); + + Assert.NotNull(imported); + Debug.Assert(imported != null); + + ensureLoaded(realmFactory.Context); + + Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("subfolder"))), "Files contain common subfolder"); + } + finally + { + Directory.Delete(extractedFolder, true); + } + }); + } + + [Test] + public void TestImportWithIgnoredDirectoryInArchive() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + string dataFolder = Path.Combine(extractedFolder, "actual_data"); + string resourceForkFolder = Path.Combine(extractedFolder, "__MACOSX"); + string resourceForkFilePath = Path.Combine(resourceForkFolder, ".extracted"); + + Directory.CreateDirectory(dataFolder); + Directory.CreateDirectory(resourceForkFolder); + + using (var resourceForkFile = File.CreateText(resourceForkFilePath)) + { + await resourceForkFile.WriteLineAsync("adding content so that it's not empty"); + } + + try + { + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(dataFolder); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + var imported = await importer.Import(new ImportTask(temp)); + + Assert.NotNull(imported); + Debug.Assert(imported != null); + + ensureLoaded(realmFactory.Context); + + Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("__MACOSX"))), "Files contain resource fork folder, which should be ignored"); + Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("actual_data"))), "Files contain common subfolder"); + } + finally + { + Directory.Delete(extractedFolder, true); + } + }); + } + + [Test] + public void TestUpdateBeatmapInfo() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using var importer = new BeatmapImporter(realmFactory, storage); + using var store = new RealmRulesetStore(realmFactory, storage); + + var temp = TestResources.GetTestBeatmapForImport(); + await importer.Import(temp); + + // Update via the beatmap, not the beatmap info, to ensure correct linking + RealmBeatmapSet setToUpdate = realmFactory.Context.All().First(); + + var beatmapToUpdate = setToUpdate.Beatmaps.First(); + + realmFactory.Context.Write(() => beatmapToUpdate.DifficultyName = "updated"); + + RealmBeatmap updatedInfo = realmFactory.Context.All().First(b => b.ID == beatmapToUpdate.ID); + Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated")); + }); + } + + public static async Task LoadQuickOszIntoOsu(BeatmapImporter importer, Realm realm) + { + var temp = TestResources.GetQuickTestBeatmapForImport(); + + var importedSet = await importer.Import(new ImportTask(temp)); + + Assert.NotNull(importedSet); + + ensureLoaded(realm); + + waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + + return realm.All().FirstOrDefault(beatmapSet => beatmapSet.ID == importedSet!.ID); + } + + public static async Task LoadOszIntoStore(BeatmapImporter importer, Realm realm, string? path = null, bool virtualTrack = false) + { + var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack); + + var importedSet = await importer.Import(new ImportTask(temp)); + + Assert.NotNull(importedSet); + Debug.Assert(importedSet != null); + + ensureLoaded(realm); + + waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + + return realm.All().First(beatmapSet => beatmapSet.ID == importedSet.ID); + } + + private void deleteBeatmapSet(RealmBeatmapSet imported, Realm realm) + { + realm.Write(() => imported.DeletePending = true); + + checkBeatmapSetCount(realm, 0); + checkBeatmapSetCount(realm, 1, true); + + Assert.IsTrue(realm.All().First(_ => true).DeletePending); + } + + private static Task createScoreForBeatmap(Realm realm, RealmBeatmap beatmap) + { + // TODO: reimplement when we have score support in realm. + // return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo + // { + // OnlineScoreID = 2, + // Beatmap = beatmap, + // BeatmapInfoID = beatmap.ID + // }, new ImportScoreTest.TestArchiveReader()); + + return Task.CompletedTask; + } + + private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false) + { + Assert.AreEqual(expected, includeDeletePending + ? realm.All().Count() + : realm.All().Count(s => !s.DeletePending)); + } + + private static string hashFile(string filename) + { + using (var s = File.OpenRead(filename)) + return s.ComputeMD5Hash(); + } + + private static void checkBeatmapCount(Realm realm, int expected) + { + Assert.AreEqual(expected, realm.All().Where(_ => true).ToList().Count); + } + + private static void checkSingleReferencedFileCount(Realm realm, int expected) + { + int singleReferencedCount = 0; + + foreach (var f in realm.All()) + { + if (f.BacklinksCount == 1) + singleReferencedCount++; + } + + Assert.AreEqual(expected, singleReferencedCount); + } + + private static void ensureLoaded(Realm realm, int timeout = 60000) + { + IQueryable? resultSets = null; + + waitForOrAssert(() => (resultSets = realm.All().Where(s => s.OnlineID == 241526)).Any(), + @"BeatmapSet did not import to the database in allocated time.", timeout); + + // ensure we were stored to beatmap database backing... + Assert.IsTrue(resultSets?.Count() == 1, $@"Incorrect result count found ({resultSets?.Count()} but should be 1)."); + + IEnumerable queryBeatmapSets() => realm.All().Where(s => !s.DeletePending && s.OnlineID == 241526); + + var set = queryBeatmapSets().First(); + + // ReSharper disable once PossibleUnintendedReferenceComparison + IEnumerable queryBeatmaps() => realm.All().Where(s => s.BeatmapSet != null && s.BeatmapSet == set); + + waitForOrAssert(() => queryBeatmaps().Count() == 12, @"Beatmaps did not import to the database in allocated time", timeout); + waitForOrAssert(() => queryBeatmapSets().Count() == 1, @"BeatmapSet did not import to the database in allocated time", timeout); + + int countBeatmapSetBeatmaps = 0; + int countBeatmaps = 0; + + waitForOrAssert(() => + (countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) == + (countBeatmaps = queryBeatmaps().Count()), + $@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout); + + foreach (RealmBeatmap b in set.Beatmaps) + Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID)); + Assert.IsTrue(set.Beatmaps.Count > 0); + } + + private static void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) + { + const int sleep = 200; + + while (timeout > 0) + { + Thread.Sleep(sleep); + timeout -= sleep; + + if (result()) + return; + } + + Assert.Fail(failureMessage); + } + } +} diff --git a/osu.Game/Database/IPostImports.cs b/osu.Game/Database/IPostImports.cs index f09285089a..b3b83f23ef 100644 --- a/osu.Game/Database/IPostImports.cs +++ b/osu.Game/Database/IPostImports.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; +#nullable enable + namespace osu.Game.Database { public interface IPostImports @@ -12,6 +14,6 @@ namespace osu.Game.Database /// /// Fired when the user requests to view the resulting import. /// - public Action>> PostImport { set; } + public Action>>? PostImport { set; } } } diff --git a/osu.Game/Stores/ArchiveModelImporter.cs b/osu.Game/Stores/ArchiveModelImporter.cs index c165dbecd8..640a031e42 100644 --- a/osu.Game/Stores/ArchiveModelImporter.cs +++ b/osu.Game/Stores/ArchiveModelImporter.cs @@ -64,7 +64,7 @@ namespace osu.Game.Stores /// /// Fired when the user requests to view the resulting import. /// - public Action>>? PresentImport; + public Action>>? PostImport { get; set; } /// /// Set an endpoint for notifications to be posted to. @@ -172,12 +172,12 @@ namespace osu.Game.Stores ? $"Imported {imported.First()}!" : $"Imported {imported.Count} {HumanisedModelName}s!"; - if (imported.Count > 0 && PresentImport != null) + if (imported.Count > 0 && PostImport != null) { notification.CompletionText += " Click to view."; notification.CompletionClickAction = () => { - PresentImport?.Invoke(imported); + PostImport?.Invoke(imported); return true; }; } From cd64faa4f91da349c23f861f4bf9d89964764950 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Oct 2021 14:33:47 +0900 Subject: [PATCH 12/79] Tidy up importer difficulty creation code --- osu.Game/Stores/BeatmapImporter.cs | 133 ++++++++++++++--------------- 1 file changed, 66 insertions(+), 67 deletions(-) diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 95d2c4fe7b..2b8615f072 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -201,102 +201,96 @@ namespace osu.Game.Stores foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) { - using (var raw = Files.Store.GetStream(file.File.StoragePath)) - using (var ms = new MemoryStream()) // we need a memory stream so we can seek - using (var sr = new LineBufferedReader(ms)) + using (var memoryStream = new MemoryStream(Files.Store.Get(file.File.StoragePath))) // we need a memory stream so we can seek { - raw.CopyTo(ms); - ms.Position = 0; + IBeatmap decoded; + using (var lineReader = new LineBufferedReader(memoryStream, true)) + decoded = Decoder.GetDecoder(lineReader).Decode(lineReader); - var decoder = Decoder.GetDecoder(sr); - IBeatmap beatmap = decoder.Decode(sr); - - string hash = ms.ComputeSHA2Hash(); + string hash = memoryStream.ComputeSHA2Hash(); if (beatmaps.Any(b => b.Hash == hash)) - continue; - - var beatmapInfo = beatmap.BeatmapInfo; - - var ruleset = realm.All().FirstOrDefault(r => r.OnlineID == beatmapInfo.RulesetID); - var rulesetInstance = (ruleset as IRulesetInfo)?.CreateInstance(); - - if (ruleset == null || rulesetInstance == null) { - Logger.Log($"Skipping import due to missing local ruleset {beatmapInfo.RulesetID}.", LoggingTarget.Database); + Logger.Log($"Skipping import of {file.Filename} due to duplicate file content.", LoggingTarget.Database); continue; } - beatmapInfo.Path = file.Filename; - beatmapInfo.Hash = hash; - beatmapInfo.MD5Hash = ms.ComputeMD5Hash(); + var decodedInfo = decoded.BeatmapInfo; + var decodedDifficulty = decodedInfo.BaseDifficulty; - // TODO: this should be done in a better place once we actually need to dynamically update it. - beatmap.BeatmapInfo.Ruleset = rulesetInstance.RulesetInfo; - beatmap.BeatmapInfo.StarDifficulty = rulesetInstance.CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating; - beatmap.BeatmapInfo.Length = calculateLength(beatmap); - beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); + var ruleset = realm.All().FirstOrDefault(r => r.OnlineID == decodedInfo.RulesetID); + + if (ruleset?.Available != true) + { + Logger.Log($"Skipping import of {file.Filename} due to missing local ruleset {decodedInfo.RulesetID}.", LoggingTarget.Database); + continue; + } var difficulty = new RealmBeatmapDifficulty { - DrainRate = beatmapInfo.BaseDifficulty.DrainRate, - CircleSize = beatmapInfo.BaseDifficulty.CircleSize, - OverallDifficulty = beatmapInfo.BaseDifficulty.OverallDifficulty, - ApproachRate = beatmapInfo.BaseDifficulty.ApproachRate, - SliderMultiplier = beatmapInfo.BaseDifficulty.SliderMultiplier, - SliderTickRate = beatmapInfo.BaseDifficulty.SliderTickRate, + DrainRate = decodedDifficulty.DrainRate, + CircleSize = decodedDifficulty.CircleSize, + OverallDifficulty = decodedDifficulty.OverallDifficulty, + ApproachRate = decodedDifficulty.ApproachRate, + SliderMultiplier = decodedDifficulty.SliderMultiplier, + SliderTickRate = decodedDifficulty.SliderTickRate, }; var metadata = new RealmBeatmapMetadata { - Title = beatmap.Metadata.Title, - TitleUnicode = beatmap.Metadata.TitleUnicode, - Artist = beatmap.Metadata.Artist, - ArtistUnicode = beatmap.Metadata.ArtistUnicode, - Author = beatmap.Metadata.AuthorString, - Source = beatmap.Metadata.Source, - Tags = beatmap.Metadata.Tags, - PreviewTime = beatmap.Metadata.PreviewTime, - AudioFile = beatmap.Metadata.AudioFile, - BackgroundFile = beatmap.Metadata.BackgroundFile, + Title = decoded.Metadata.Title, + TitleUnicode = decoded.Metadata.TitleUnicode, + Artist = decoded.Metadata.Artist, + ArtistUnicode = decoded.Metadata.ArtistUnicode, + Author = decoded.Metadata.AuthorString, + Source = decoded.Metadata.Source, + Tags = decoded.Metadata.Tags, + PreviewTime = decoded.Metadata.PreviewTime, + AudioFile = decoded.Metadata.AudioFile, + BackgroundFile = decoded.Metadata.BackgroundFile, }; - var realmBeatmap = new RealmBeatmap(ruleset, difficulty, metadata) + var beatmap = new RealmBeatmap(ruleset, difficulty, metadata) { - DifficultyName = beatmapInfo.Version, - OnlineID = beatmapInfo.OnlineBeatmapID, - Length = beatmapInfo.Length, - BPM = beatmapInfo.BPM, - Hash = beatmapInfo.Hash, - StarRating = beatmapInfo.StarDifficulty, - MD5Hash = beatmapInfo.MD5Hash, - Hidden = beatmapInfo.Hidden, - AudioLeadIn = beatmapInfo.AudioLeadIn, - StackLeniency = beatmapInfo.StackLeniency, - SpecialStyle = beatmapInfo.SpecialStyle, - LetterboxInBreaks = beatmapInfo.LetterboxInBreaks, - WidescreenStoryboard = beatmapInfo.WidescreenStoryboard, - EpilepsyWarning = beatmapInfo.EpilepsyWarning, - SamplesMatchPlaybackRate = beatmapInfo.SamplesMatchPlaybackRate, - DistanceSpacing = beatmapInfo.DistanceSpacing, - BeatDivisor = beatmapInfo.BeatDivisor, - GridSize = beatmapInfo.GridSize, - TimelineZoom = beatmapInfo.TimelineZoom, + Hash = hash, + DifficultyName = decodedInfo.Version, + OnlineID = decodedInfo.OnlineBeatmapID, + AudioLeadIn = decodedInfo.AudioLeadIn, + StackLeniency = decodedInfo.StackLeniency, + SpecialStyle = decodedInfo.SpecialStyle, + LetterboxInBreaks = decodedInfo.LetterboxInBreaks, + WidescreenStoryboard = decodedInfo.WidescreenStoryboard, + EpilepsyWarning = decodedInfo.EpilepsyWarning, + SamplesMatchPlaybackRate = decodedInfo.SamplesMatchPlaybackRate, + DistanceSpacing = decodedInfo.DistanceSpacing, + BeatDivisor = decodedInfo.BeatDivisor, + GridSize = decodedInfo.GridSize, + TimelineZoom = decodedInfo.TimelineZoom, + MD5Hash = memoryStream.ComputeMD5Hash(), }; - // TODO: IBeatmap.BeatmapInfo needs to be updated to the new interface. - // beatmaps.Add(beatmap.BeatmapInfo); + updateBeatmapStatistics(beatmap, decoded); - beatmaps.Add(realmBeatmap); + beatmaps.Add(beatmap); } } return beatmaps; } - public void Dispose() + private void updateBeatmapStatistics(RealmBeatmap beatmap, IBeatmap decoded) { - onlineLookupQueue?.Dispose(); + var rulesetInstance = ((IRulesetInfo)beatmap.Ruleset).CreateInstance(); + + if (rulesetInstance == null) + return; + + decoded.BeatmapInfo.Ruleset = rulesetInstance.RulesetInfo; + + // TODO: this should be done in a better place once we actually need to dynamically update it. + beatmap.StarRating = rulesetInstance.CreateDifficultyCalculator(new DummyConversionBeatmap(decoded)).Calculate().StarRating; + beatmap.Length = calculateLength(decoded); + beatmap.BPM = 60000 / decoded.GetMostCommonBeatLength(); } private double calculateLength(IBeatmap b) @@ -313,6 +307,11 @@ namespace osu.Game.Stores return endTime - startTime; } + public void Dispose() + { + onlineLookupQueue?.Dispose(); + } + /// /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. /// From 0e1f144bf4519463f6fcc968009c032c7eee02dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Oct 2021 17:08:43 +0900 Subject: [PATCH 13/79] Rename `ArchiveModelImporter` with `Realm` prefix to avoid confusion --- osu.Game/Stores/BeatmapImporter.cs | 2 +- ...veModelImporter.cs => RealmArchiveModelImporter.cs} | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) rename osu.Game/Stores/{ArchiveModelImporter.cs => RealmArchiveModelImporter.cs} (98%) diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 2b8615f072..33276bff2f 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -34,7 +34,7 @@ namespace osu.Game.Stores /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// [ExcludeFromDynamicCompile] - public class BeatmapImporter : ArchiveModelImporter, IDisposable + public class BeatmapImporter : RealmArchiveModelImporter, IDisposable { public override IEnumerable HandledExtensions => new[] { ".osz" }; diff --git a/osu.Game/Stores/ArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs similarity index 98% rename from osu.Game/Stores/ArchiveModelImporter.cs rename to osu.Game/Stores/RealmArchiveModelImporter.cs index 640a031e42..cc61896ea5 100644 --- a/osu.Game/Stores/ArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -29,7 +29,7 @@ namespace osu.Game.Stores /// Adds cross-functionality with to give access to the central file store for the provided model. /// /// The model type. - public abstract class ArchiveModelImporter : IModelImporter + public abstract class RealmArchiveModelImporter : IModelImporter where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete { private const int import_queue_request_concurrency = 1; @@ -40,20 +40,20 @@ namespace osu.Game.Stores private const int low_priority_import_batch_size = 1; /// - /// A singleton scheduler shared by all . + /// A singleton scheduler shared by all . /// /// /// This scheduler generally performs IO and CPU intensive work so concurrency is limited harshly. /// It is mainly being used as a queue mechanism for large imports. /// - private static readonly ThreadedTaskScheduler import_scheduler = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelImporter)); + private static readonly ThreadedTaskScheduler import_scheduler = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(RealmArchiveModelImporter)); /// /// A second scheduler for lower priority imports. /// For simplicity, these will just run in parallel with normal priority imports, but a future refactor would see this implemented via a custom scheduler/queue. /// See https://gist.github.com/peppy/f0e118a14751fc832ca30dd48ba3876b for an incomplete version of this. /// - private static readonly ThreadedTaskScheduler import_scheduler_low_priority = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelImporter)); + private static readonly ThreadedTaskScheduler import_scheduler_low_priority = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(RealmArchiveModelImporter)); public virtual IEnumerable HandledExtensions => new[] { @".zip" }; @@ -71,7 +71,7 @@ namespace osu.Game.Stores /// public Action? PostNotification { protected get; set; } - protected ArchiveModelImporter(Storage storage, RealmContextFactory contextFactory) + protected RealmArchiveModelImporter(Storage storage, RealmContextFactory contextFactory) { ContextFactory = contextFactory; From c8d99e68a59a9f08f5daa423ba004bb072897b98 Mon Sep 17 00:00:00 2001 From: StanR Date: Fri, 15 Oct 2021 16:51:05 +0300 Subject: [PATCH 14/79] Remove calculation for scores with combo above threshold, avoid division by zero --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index d0b5e877b5..3895389f61 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -262,9 +262,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount; if (scoreMaxCombo < fullComboThreshold) - comboBasedMissCount = fullComboThreshold / scoreMaxCombo; - else - comboBasedMissCount = Math.Pow((Attributes.MaxCombo - scoreMaxCombo) / (0.1 * Attributes.SliderCount), 3); + comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); } return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount)); From 29ec498f6c69f779ee55d78c08299109ead9283c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Oct 2021 13:34:24 +0200 Subject: [PATCH 15/79] Add failing test case for player sending wrong ruleset ID to spectator server --- .../Visual/Gameplay/TestSceneSpectatorHost.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs new file mode 100644 index 0000000000..89fea1f92d --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Online.API; +using osu.Game.Online.Spectator; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mania; +using osu.Game.Tests.Visual.Spectator; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneSpectatorHost : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [Cached(typeof(SpectatorClient))] + private TestSpectatorClient spectatorClient { get; } = new TestSpectatorClient(); + + private DummyAPIAccess dummyAPIAccess => (DummyAPIAccess)API; + private const int dummy_user_id = 42; + + public override void SetUpSteps() + { + AddStep("set dummy user", () => dummyAPIAccess.LocalUser.Value = new User + { + Id = dummy_user_id, + Username = "DummyUser" + }); + AddStep("add test spectator client", () => Add(spectatorClient)); + AddStep("add watching user", () => spectatorClient.WatchUser(dummy_user_id)); + base.SetUpSteps(); + } + + [Test] + public void TestClientSendsCorrectRuleset() + { + AddUntilStep("spectator client sending frames", () => spectatorClient.PlayingUserStates.ContainsKey(dummy_user_id)); + AddAssert("spectator client sent correct ruleset", () => spectatorClient.PlayingUserStates[dummy_user_id].RulesetID == Ruleset.Value.ID); + } + + public override void TearDownSteps() + { + base.TearDownSteps(); + AddStep("stop watching user", () => spectatorClient.StopWatchingUser(dummy_user_id)); + AddStep("remove test spectator client", () => Remove(spectatorClient)); + } + } +} From e57d6e930eb3246da0279278e9bbf085103c191d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Oct 2021 13:48:49 +0200 Subject: [PATCH 16/79] Source spectator state sent to server from gameplay state --- osu.Game/Online/Spectator/SpectatorClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index b597b2f214..94bb11a7bc 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -144,9 +144,9 @@ namespace osu.Game.Online.Spectator IsPlaying = true; // transfer state at point of beginning play - currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineBeatmapID; - currentState.RulesetID = score.ScoreInfo.RulesetID; - currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); + currentState.BeatmapID = state.Beatmap.BeatmapInfo.OnlineBeatmapID; + currentState.RulesetID = state.Ruleset.RulesetInfo.ID; + currentState.Mods = state.Mods.Select(m => new APIMod(m)).ToArray(); currentBeatmap = state.Beatmap; currentScore = score; From ccaac977947174af7d8de690f90f5977b230242d Mon Sep 17 00:00:00 2001 From: StanR Date: Sat, 16 Oct 2021 14:50:15 +0300 Subject: [PATCH 17/79] Clamp comboBasedMissCount --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 3895389f61..4bca87204a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -265,6 +265,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); } + // we're clamping misscount because since its derived from combo it can be higher than total hits and that breaks some calculations + comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits); + return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount)); } From b34086a792c9865f990cb289655efeb2930710fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Oct 2021 16:03:13 +0200 Subject: [PATCH 18/79] Add failing test case for incorrect `RulesetID` population in submission flow --- .../TestScenePlayerScoreSubmission.cs | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index 5ff2e9c439..d7edb33c42 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Screens; using osu.Game.Beatmaps; @@ -10,10 +11,13 @@ using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Online.Solo; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko; +using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; @@ -32,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override bool HasCustomSteps => true; - protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false); + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new NonImportingPlayer(false); protected override Ruleset CreatePlayerRuleset() => createCustomRuleset?.Invoke() ?? new OsuRuleset(); @@ -86,6 +90,46 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true); } + [Test] + public void TestSubmissionForDifferentRuleset() + { + prepareTokenResponse(true); + + createPlayerTest(createRuleset: () => new TaikoRuleset()); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); + + addFakeHit(); + + AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); + + AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen); + AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true); + AddAssert("submitted score has correct ruleset ID", () => Player.SubmittedScore?.ScoreInfo.RulesetID == new TaikoRuleset().RulesetInfo.ID); + } + + [Test] + public void TestSubmissionForConvertedBeatmap() + { + prepareTokenResponse(true); + + createPlayerTest(createRuleset: () => new ManiaRuleset(), createBeatmap: _ => createTestBeatmap(new OsuRuleset().RulesetInfo)); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); + + addFakeHit(); + + AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); + + AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen); + AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true); + AddAssert("submitted score has correct ruleset ID", () => Player.SubmittedScore?.ScoreInfo.RulesetID == new ManiaRuleset().RulesetInfo.ID); + } + [Test] public void TestNoSubmissionOnExitWithNoToken() { @@ -242,5 +286,33 @@ namespace osu.Game.Tests.Visual.Gameplay }); }); } + + private class NonImportingPlayer : TestPlayer + { + public NonImportingPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) + : base(allowPause, showResults, pauseOnFocusLost) + { + } + + protected override Task ImportScore(Score score) + { + // It was discovered that Score members could sometimes be half-populated. + // In particular, the RulesetID property could be set to 0 even on non-osu! maps. + // We want to test that the state of that property is consistent in this test. + // EF makes this impossible. + // + // First off, because of the EF navigational property-explicit foreign key field duality, + // it can happen that - for example - the Ruleset navigational property is correctly initialised to mania, + // but the RulesetID foreign key property is not initialised and remains 0. + // EF silently bypasses this by prioritising the Ruleset navigational property over the RulesetID foreign key one. + // + // Additionally, adding an entity to an EF DbSet CAUSES SIDE EFFECTS with regard to the foreign key property. + // In the above instance, if a ScoreInfo with Ruleset = {mania} and RulesetID = 0 is attached to an EF context, + // RulesetID WILL BE SILENTLY SET TO THE CORRECT VALUE of 3. + // + // For the above reasons, importing is disabled in this test. + return Task.CompletedTask; + } + } } } From 874decb3cdbd6bde57b0616addcc67e44542b734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Oct 2021 16:06:20 +0200 Subject: [PATCH 19/79] Replace spectator-local fix for wrong ruleset ID with player-global consistency check --- osu.Game/Online/Spectator/SpectatorClient.cs | 6 +++--- osu.Game/Screens/Play/Player.cs | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 94bb11a7bc..b597b2f214 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -144,9 +144,9 @@ namespace osu.Game.Online.Spectator IsPlaying = true; // transfer state at point of beginning play - currentState.BeatmapID = state.Beatmap.BeatmapInfo.OnlineBeatmapID; - currentState.RulesetID = state.Ruleset.RulesetInfo.ID; - currentState.Mods = state.Mods.Select(m => new APIMod(m)).ToArray(); + currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineBeatmapID; + currentState.RulesetID = score.ScoreInfo.RulesetID; + currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); currentBeatmap = state.Beatmap; currentScore = score; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 5398a955b3..c8b946b7f1 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.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.Linq; using System.Threading.Tasks; @@ -217,9 +218,12 @@ namespace osu.Game.Screens.Play Score = CreateScore(playableBeatmap); + Debug.Assert(ruleset.RulesetInfo.ID != null); + // ensure the score is in a consistent state with the current player. Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; + Score.ScoreInfo.RulesetID = ruleset.RulesetInfo.ID.Value; Score.ScoreInfo.Mods = gameplayMods; dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score)); From 3529f34c98b09399448a8aa6f1459e1460275361 Mon Sep 17 00:00:00 2001 From: 5ln Date: Mon, 18 Oct 2021 01:58:38 +0800 Subject: [PATCH 20/79] Multi: Hide mods when spectating or Beatmap isn't Locally Available. Signed-off-by: 5ln --- .../TestSceneMultiplayerParticipantsList.cs | 62 +++++++++++++++++++ .../Participants/ParticipantPanel.cs | 6 ++ 2 files changed, 68 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index c4ebc13245..d1980b03c7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -275,6 +275,68 @@ namespace osu.Game.Tests.Visual.Multiplayer var state = i; AddStep($"set state: {state}", () => Client.ChangeUserState(0, state)); } + + AddStep("set state: downloading", () => Client.ChangeUserBeatmapAvailability(0, BeatmapAvailability.Downloading(0))); + + AddStep("set state: locally available", () => Client.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable())); + } + + [Test] + public void TestModOverlap() + { + AddStep("add dummy mods", () => + { + Client.ChangeUserMods(new Mod[] + { + new OsuModNoFail(), + new OsuModDoubleTime() + }); + }); + + AddStep("add user with mods", () => + { + Client.AddUser(new User + { + Id = 0, + Username = "Baka", + RulesetsStatistics = new Dictionary + { + { + Ruleset.Value.ShortName, + new UserStatistics { GlobalRank = RNG.Next(1, 100000), } + } + }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + Client.ChangeUserMods(0, new Mod[] + { + new OsuModHardRock(), + new OsuModDoubleTime() + }); + }); + + AddStep("set 0 ready", () => Client.ChangeState(MultiplayerUserState.Ready)); + + AddStep("set 1 spectate", () => Client.ChangeUserState(0, MultiplayerUserState.Spectating)); + + // Have to set back to idle due to status priority. + AddStep("set 0 no map, 1 ready", () => + { + Client.ChangeState(MultiplayerUserState.Idle); + Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()); + Client.ChangeUserState(0, MultiplayerUserState.Ready); + }); + + AddStep("set 0 downloading", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); + + AddStep("set 0 spectate", () => Client.ChangeUserState(0, MultiplayerUserState.Spectating)); + + AddStep("make both default", () => + { + Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()); + Client.ChangeUserState(0, MultiplayerUserState.Idle); + Client.ChangeState(MultiplayerUserState.Idle); + }); } private void createNewParticipantsList() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 6f8c735b6e..79e305b765 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; @@ -190,6 +191,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); + if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating)) + userModsDisplay.FadeIn(fade_time); + else + userModsDisplay.FadeOut(fade_time); + if (Client.IsHost && !User.Equals(Client.LocalUser)) kickButton.FadeIn(fade_time); else From 818f35c35f00724e30f4dbbd51fc58a98386627d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Oct 2021 21:44:56 +0200 Subject: [PATCH 21/79] Restyle default value indicator --- .../TestSceneRestoreDefaultValueButton.cs | 53 +++++++++++++++ osu.Game/Graphics/OsuColour.cs | 7 ++ .../Overlays/RestoreDefaultValueButton.cs | 66 ++++++++++--------- 3 files changed, 96 insertions(+), 30 deletions(-) create mode 100644 osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs b/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs new file mode 100644 index 0000000000..0716907315 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Tests.Visual.Settings +{ + public class TestSceneRestoreDefaultValueButton : OsuTestScene + { + [Resolved] + private OsuColour colours { get; set; } + + [Test] + public void TestBasic() + { + RestoreDefaultValueButton restoreDefaultValueButton = null; + + AddStep("create button", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.GreySeafoam + }, + restoreDefaultValueButton = new RestoreDefaultValueButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = new BindableBool(), + } + } + }); + AddSliderStep("set scale", 1, 4, 1, scale => + { + if (restoreDefaultValueButton != null) + restoreDefaultValueButton.Scale = new Vector2(scale); + }); + AddToggleStep("toggle default state", state => restoreDefaultValueButton.Current.Value = state); + AddToggleStep("toggle disabled state", state => restoreDefaultValueButton.Current.Disabled = state); + } + } +} diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index af2bb26871..40d163635a 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -235,11 +235,18 @@ namespace osu.Game.Graphics /// public readonly Color4 Blue3 = Color4Extensions.FromHex(@"3399cc"); + public readonly Color4 Lime0 = Color4Extensions.FromHex(@"ccff99"); + /// /// Equivalent to 's . /// public readonly Color4 Lime1 = Color4Extensions.FromHex(@"b2ff66"); + /// + /// Equivalent to 's . + /// + public readonly Color4 Lime3 = Color4Extensions.FromHex(@"7fcc33"); + /// /// Equivalent to 's . /// diff --git a/osu.Game/Overlays/RestoreDefaultValueButton.cs b/osu.Game/Overlays/RestoreDefaultValueButton.cs index 87a294cc10..de600e8172 100644 --- a/osu.Game/Overlays/RestoreDefaultValueButton.cs +++ b/osu.Game/Overlays/RestoreDefaultValueButton.cs @@ -3,10 +3,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.UserInterface; @@ -14,6 +12,7 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; +using osuTK; namespace osu.Game.Overlays { @@ -45,30 +44,21 @@ namespace osu.Game.Overlays } } - private bool hovering; + [Resolved] + private OsuColour colours { get; set; } - public RestoreDefaultValueButton() - { - Height = 1; - - RelativeSizeAxes = Axes.Y; - Width = SettingsPanel.CONTENT_MARGINS; - } + private const float size = 4; [BackgroundDependencyLoader] private void load(OsuColour colour) { - BackgroundColour = colour.Yellow; - Content.Width = 0.33f; - Content.CornerRadius = 3; - Content.EdgeEffect = new EdgeEffectParameters - { - Colour = BackgroundColour.Opacity(0.1f), - Type = EdgeEffectType.Glow, - Radius = 2, - }; + BackgroundColour = colour.Lime1; + Size = new Vector2(3 * size); + + Content.RelativeSizeAxes = Axes.None; + Content.Size = new Vector2(size); + Content.CornerRadius = size / 2; - Padding = new MarginPadding { Vertical = 1.5f }; Alpha = 0f; Action += () => @@ -81,39 +71,55 @@ namespace osu.Game.Overlays protected override void LoadComplete() { base.LoadComplete(); - - // avoid unnecessary transforms on first display. - Alpha = currentAlpha; - Background.Colour = currentColour; + updateState(); + FinishTransforms(true); } public LocalisableString TooltipText => "revert to default"; protected override bool OnHover(HoverEvent e) { - hovering = true; UpdateState(); return false; } protected override void OnHoverLost(HoverLostEvent e) { - hovering = false; UpdateState(); } public void UpdateState() => Scheduler.AddOnce(updateState); - private float currentAlpha => current.IsDefault ? 0f : hovering && !current.Disabled ? 1f : 0.65f; - private ColourInfo currentColour => current.Disabled ? Color4.Gray : BackgroundColour; + private const double fade_duration = 200; private void updateState() { if (current == null) return; - this.FadeTo(currentAlpha, 200, Easing.OutQuint); - Background.FadeColour(currentColour, 200, Easing.OutQuint); + Enabled.Value = !Current.Disabled; + + if (!Current.Disabled) + { + this.FadeTo(Current.IsDefault ? 0 : 1, fade_duration, Easing.OutQuint); + Background.FadeColour(IsHovered ? colours.Lime0 : colours.Lime1, fade_duration, Easing.OutQuint); + Content.TweenEdgeEffectTo(new EdgeEffectParameters + { + Colour = (IsHovered ? colours.Lime1 : colours.Lime3).Opacity(0.4f), + Radius = IsHovered ? 8 : 4, + Type = EdgeEffectType.Glow + }, fade_duration, Easing.OutQuint); + } + else + { + Background.FadeColour(colours.Lime3, fade_duration, Easing.OutQuint); + Content.TweenEdgeEffectTo(new EdgeEffectParameters + { + Colour = colours.Lime3.Opacity(0.4f).Opacity(0.1f), + Radius = 2, + Type = EdgeEffectType.Glow + }, fade_duration, Easing.OutQuint); + } } } } From f422ebb281edd47e30968ba8ec7d38ca390fe382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Oct 2021 22:44:28 +0200 Subject: [PATCH 22/79] Adjust `SettingsItem` to accommodate new default value indicator --- .../Visual/Settings/TestSceneSettingsItem.cs | 45 ++++++++++++-- .../Overlays/Settings/SettingsDropdown.cs | 6 -- osu.Game/Overlays/Settings/SettingsItem.cs | 61 +++++++++++++++---- osu.Game/Overlays/Settings/SettingsSlider.cs | 1 - 4 files changed, 90 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs index d9cce69ee3..9c3940af4c 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs @@ -4,6 +4,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Overlays.Settings; using osu.Game.Overlays; @@ -29,9 +30,10 @@ namespace osu.Game.Tests.Visual.Settings Value = "test" } }; - - restoreDefaultValueButton = textBox.ChildrenOfType>().Single(); }); + AddUntilStep("wait for loaded", () => textBox.IsLoaded); + AddStep("retrieve restore default button", () => restoreDefaultValueButton = textBox.ChildrenOfType>().Single()); + AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); AddStep("change value from default", () => textBox.Current.Value = "non-default"); @@ -41,6 +43,41 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); } + [Test] + public void TestSetAndClearLabelText() + { + SettingsTextBox textBox = null; + GridContainer settingsItemGrid = null; + RestoreDefaultValueButton restoreDefaultValueButton = null; + + AddStep("create settings item", () => + { + Child = textBox = new SettingsTextBox + { + Current = new Bindable + { + Default = "test", + Value = "test" + } + }; + }); + AddUntilStep("wait for loaded", () => textBox.IsLoaded); + AddStep("retrieve components", () => + { + settingsItemGrid = textBox.ChildrenOfType().Single(); + restoreDefaultValueButton = textBox.ChildrenOfType>().Single(); + }); + + AddStep("set non-default value", () => restoreDefaultValueButton.Current.Value = "non-default"); + AddAssert("default value button next to control", () => settingsItemGrid.Content[1][0] == restoreDefaultValueButton); + + AddStep("set label", () => textBox.LabelText = "label text"); + AddAssert("default value button next to label", () => settingsItemGrid.Content[0][0] == restoreDefaultValueButton); + + AddStep("clear label", () => textBox.LabelText = default); + AddAssert("default value button next to control", () => settingsItemGrid.Content[1][0] == restoreDefaultValueButton); + } + /// /// Ensures that the reset to default button uses the correct implementation of IsDefault to determine whether it should be shown or not. /// Values have been chosen so that after being set, Value != Default (but they are close enough that the difference is negligible compared to Precision). @@ -64,9 +101,9 @@ namespace osu.Game.Tests.Visual.Settings Precision = 0.1f, } }; - - restoreDefaultValueButton = sliderBar.ChildrenOfType>().Single(); }); + AddUntilStep("wait for loaded", () => sliderBar.IsLoaded); + AddStep("retrieve restore default button", () => restoreDefaultValueButton = sliderBar.ChildrenOfType>().Single()); AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); diff --git a/osu.Game/Overlays/Settings/SettingsDropdown.cs b/osu.Game/Overlays/Settings/SettingsDropdown.cs index a281d03ee7..1e90222d28 100644 --- a/osu.Game/Overlays/Settings/SettingsDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; -using osuTK; namespace osu.Game.Overlays.Settings { @@ -28,11 +27,6 @@ namespace osu.Game.Overlays.Settings public override IEnumerable FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.ToString())); - public SettingsDropdown() - { - FlowContent.Spacing = new Vector2(0, 10); - } - protected sealed override Drawable CreateControl() => CreateDropdown(); protected virtual OsuDropdown CreateDropdown() => new DropdownControl(); diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 5282217013..7895f6386b 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -14,6 +15,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Containers; +using osuTK; namespace osu.Game.Overlays.Settings { @@ -30,6 +32,10 @@ namespace osu.Game.Overlays.Settings protected readonly FillFlowContainer FlowContent; private SpriteText labelText; + private readonly GridContainer gridContainer; + + [CanBeNull] + private RestoreDefaultValueButton defaultValueButton; private OsuTextFlowContainer warningText; @@ -48,12 +54,13 @@ namespace osu.Game.Overlays.Settings if (labelText == null) { // construct lazily for cases where the label is not needed (may be provided by the Control). - FlowContent.Insert(-1, labelText = new OsuSpriteText()); + gridContainer.Content[0][1] = labelText = new OsuSpriteText(); updateDisabled(); } labelText.Text = value; + updateLayout(); } } @@ -106,18 +113,33 @@ namespace osu.Game.Overlays.Settings AutoSizeAxes = Axes.Y; Padding = new MarginPadding { Right = SettingsPanel.CONTENT_MARGINS }; - InternalChildren = new Drawable[] + FlowContent = new FillFlowContainer { - FlowContent = new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 10), + Child = Control = CreateControl(), + }; + + InternalChild = gridContainer = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS }, - Children = new[] - { - Control = CreateControl(), - }, + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, SettingsPanel.CONTENT_MARGINS), + new Dimension() + }, + Content = new[] + { + new Drawable[2], + new Drawable[] { null, FlowContent } + } }; // IMPORTANT: all bindable logic is in constructor intentionally to support "CreateSettingsControls" being used in a context it is @@ -135,13 +157,28 @@ namespace osu.Game.Overlays.Settings // intentionally done before LoadComplete to avoid overhead. if (ShowsDefaultIndicator) { - AddInternal(new RestoreDefaultValueButton + defaultValueButton = new RestoreDefaultValueButton { Current = controlWithCurrent.Current, - }); + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }; + updateLayout(); } } + private void updateLayout() + { + bool hasLabel = !string.IsNullOrEmpty(labelText?.Text.ToString()); + + gridContainer.Content[0][0] = null; + gridContainer.Content[1][0] = null; + + gridContainer.Content[hasLabel ? 0 : 1][0] = defaultValueButton; + + FlowContent.Margin = new MarginPadding { Top = hasLabel ? 10 : 0 }; + } + private void updateDisabled() { if (labelText != null) diff --git a/osu.Game/Overlays/Settings/SettingsSlider.cs b/osu.Game/Overlays/Settings/SettingsSlider.cs index bb9c0dd4d7..b95b0af11c 100644 --- a/osu.Game/Overlays/Settings/SettingsSlider.cs +++ b/osu.Game/Overlays/Settings/SettingsSlider.cs @@ -19,7 +19,6 @@ namespace osu.Game.Overlays.Settings { protected override Drawable CreateControl() => new TSlider { - Margin = new MarginPadding { Vertical = 10 }, RelativeSizeAxes = Axes.X }; From 552fc1dc8af353a6a9fdc61cf8a732789f8cc6bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Oct 2021 23:15:28 +0200 Subject: [PATCH 23/79] Adjust key binding rows to accommodate new default value indicator --- .../Settings/Sections/Input/KeyBindingRow.cs | 110 ++++++++++-------- 1 file changed, 64 insertions(+), 46 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index da789db79a..c2667fbdac 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -82,64 +82,82 @@ namespace osu.Game.Overlays.Settings.Sections.Input { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }; - InternalChildren = new Drawable[] + InternalChild = new GridContainer { - new RestoreDefaultValueButton + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] { - Current = isDefault, - Action = RestoreDefaults, - Origin = Anchor.TopRight, + new Dimension(GridSizeMode.Absolute, SettingsPanel.CONTENT_MARGINS), + new Dimension(), + new Dimension(GridSizeMode.Absolute, SettingsPanel.CONTENT_MARGINS), }, - content = new Container + RowDimensions = new[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Masking = true, - CornerRadius = padding, - EdgeEffect = new EdgeEffectParameters + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] { - Radius = 2, - Colour = colourProvider.Highlight1.Opacity(0), - Type = EdgeEffectType.Shadow, - Hollow = true, - }, - Children = new Drawable[] - { - new Box + new RestoreDefaultValueButton { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5, + Current = isDefault, + Action = RestoreDefaults, + Anchor = Anchor.Centre, + Origin = Anchor.Centre }, - text = new OsuSpriteText + content = new Container { - Text = action.GetLocalisableDescription(), - Margin = new MarginPadding(1.5f * padding), - }, - buttons = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight - }, - cancelAndClearButtons = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding(padding) { Top = height + padding * 2 }, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Alpha = 0, - Spacing = new Vector2(5), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Masking = true, + CornerRadius = padding, + EdgeEffect = new EdgeEffectParameters + { + Radius = 2, + Colour = colourProvider.Highlight1.Opacity(0), + Type = EdgeEffectType.Shadow, + Hollow = true, + }, Children = new Drawable[] { - new CancelButton { Action = finalise }, - new ClearButton { Action = clear }, - }, - } + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + text = new OsuSpriteText + { + Text = action.GetLocalisableDescription(), + Margin = new MarginPadding(1.5f * padding), + }, + buttons = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight + }, + cancelAndClearButtons = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(padding) { Top = height + padding * 2 }, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Alpha = 0, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new CancelButton { Action = finalise }, + new ClearButton { Action = clear }, + }, + } + } + }, + new HoverClickSounds() } - }, - new HoverClickSounds() + } }; foreach (var b in bindings) From 2a41e8bd1fd2d5b87922f167e02302d090b4fcf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Oct 2021 23:24:53 +0200 Subject: [PATCH 24/79] Remove unneeded extra padding from settings number box --- osu.Game/Overlays/Settings/SettingsNumberBox.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index d36aa2bfc2..aca7a210b3 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -13,7 +13,6 @@ namespace osu.Game.Overlays.Settings protected override Drawable CreateControl() => new NumberControl { RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 5 } }; private sealed class NumberControl : CompositeDrawable, IHasCurrentValue From 0d992a04930c65ab24902b82d959e15bef15bfc3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Oct 2021 13:30:24 +0900 Subject: [PATCH 25/79] Add failing test showing epilepsy warning is not fading on early exit --- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index aee15a145c..ba0ee5ac6e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -291,7 +291,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => this.ChildrenOfType().Any() == warning); + AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => (getWarning() != null) == warning); if (warning) { @@ -335,12 +335,17 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddUntilStep("wait for epilepsy warning", () => loader.ChildrenOfType().Single().Alpha > 0); + AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0); + AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible); + AddStep("exit early", () => loader.Exit()); + AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden); AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); } + private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(); + private class TestPlayerLoader : PlayerLoader { public new VisualSettings VisualSettings => base.VisualSettings; From 59dc04017eac9c55a6e1982fabcca255035e2fcb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Oct 2021 13:30:37 +0900 Subject: [PATCH 26/79] Fix epilepsy warning not being faded out on an early exit from `PlayerLoader` --- osu.Game/Screens/Play/PlayerLoader.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index cf5bff57cf..d852ac2940 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -246,6 +246,9 @@ namespace osu.Game.Screens.Play cancelLoad(); contentOut(); + // If the load sequence was interrupted, the epilepsy warning may already be displayed (or in the process of being displayed). + epilepsyWarning?.Hide(); + // Ensure the screen doesn't expire until all the outwards fade operations have completed. this.Delay(content_out_duration).FadeOut(); From 50bde0fe381c6c1d4f160845935e05ea1d2a8c24 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Oct 2021 13:53:27 +0900 Subject: [PATCH 27/79] Refactor test to better keep existing toggle values I also changed the type of the button to `float` because it was mentally hard to parse a default button that is tracking a `bool` state. Probably not what we want for a test like this. --- .../TestSceneRestoreDefaultValueButton.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs b/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs index 0716907315..3eb7a77600 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs @@ -18,10 +18,18 @@ namespace osu.Game.Tests.Visual.Settings [Resolved] private OsuColour colours { get; set; } + private float scale = 1; + + private readonly Bindable current = new Bindable + { + Default = default, + Value = 1, + }; + [Test] public void TestBasic() { - RestoreDefaultValueButton restoreDefaultValueButton = null; + RestoreDefaultValueButton restoreDefaultValueButton = null; AddStep("create button", () => Child = new Container { @@ -33,21 +41,23 @@ namespace osu.Game.Tests.Visual.Settings RelativeSizeAxes = Axes.Both, Colour = colours.GreySeafoam }, - restoreDefaultValueButton = new RestoreDefaultValueButton + restoreDefaultValueButton = new RestoreDefaultValueButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Current = new BindableBool(), + Scale = new Vector2(scale), + Current = current, } } }); AddSliderStep("set scale", 1, 4, 1, scale => { + this.scale = scale; if (restoreDefaultValueButton != null) restoreDefaultValueButton.Scale = new Vector2(scale); }); - AddToggleStep("toggle default state", state => restoreDefaultValueButton.Current.Value = state); - AddToggleStep("toggle disabled state", state => restoreDefaultValueButton.Current.Disabled = state); + AddToggleStep("toggle default state", state => current.Value = state ? default : 1); + AddToggleStep("toggle disabled state", state => current.Disabled = state); } } } From d37913a8b44fecbba70ba4f3e3e2ed614dee3482 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Oct 2021 13:59:25 +0900 Subject: [PATCH 28/79] Disable null check pattern type check syntax inspections --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index e42b30e944..3af986543e 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -73,6 +73,7 @@ HINT WARNING HINT + DO_NOT_SHOW WARNING DO_NOT_SHOW WARNING From 6d6eed61aa3bbe5fa9eed81e69e2e71f79e760ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Oct 2021 14:00:35 +0900 Subject: [PATCH 29/79] Fix new indentation inspections --- osu.Game/Online/API/OAuth.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index d79fc58d1c..0dbc3b02db 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -40,12 +40,12 @@ namespace osu.Game.Online.API if (string.IsNullOrEmpty(password)) throw new ArgumentException("Missing password."); using (var req = new AccessTokenRequestPassword(username, password) - { - Url = $@"{endpoint}/oauth/token", - Method = HttpMethod.Post, - ClientId = clientId, - ClientSecret = clientSecret - }) + { + Url = $@"{endpoint}/oauth/token", + Method = HttpMethod.Post, + ClientId = clientId, + ClientSecret = clientSecret + }) { try { @@ -80,12 +80,12 @@ namespace osu.Game.Online.API try { using (var req = new AccessTokenRequestRefresh(refresh) - { - Url = $@"{endpoint}/oauth/token", - Method = HttpMethod.Post, - ClientId = clientId, - ClientSecret = clientSecret - }) + { + Url = $@"{endpoint}/oauth/token", + Method = HttpMethod.Post, + ClientId = clientId, + ClientSecret = clientSecret + }) { req.Perform(); From 762949f49fce8aa15fee93053dc766b0a6c1f58a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Oct 2021 14:20:38 +0900 Subject: [PATCH 30/79] Don't flash screen red on fail if the user has disabled red tinting --- .../Visual/Gameplay/TestSceneAllRulesetPlayers.cs | 9 +++++---- .../Visual/Gameplay/TestSceneFailAnimation.cs | 10 ++++++++++ osu.Game/Screens/Play/FailAnimation.cs | 11 ++++++++--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs index 00b5c38e20..c5ab3974a4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs @@ -20,14 +20,15 @@ namespace osu.Game.Tests.Visual.Gameplay /// public abstract class TestSceneAllRulesetPlayers : RateAdjustedBeatmapTestScene { - protected Player Player; + protected Player Player { get; private set; } + + protected OsuConfigManager Config { get; private set; } [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - OsuConfigManager manager; - Dependencies.Cache(manager = new OsuConfigManager(LocalStorage)); - manager.GetBindable(OsuSetting.DimLevel).Value = 1.0; + Dependencies.Cache(Config = new OsuConfigManager(LocalStorage)); + Config.GetBindable(OsuSetting.DimLevel).Value = 1.0; } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs index 85aaf20a19..36fc6812bd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using NUnit.Framework; using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; @@ -17,6 +19,14 @@ namespace osu.Game.Tests.Visual.Gameplay return new FailPlayer(); } + [Test] + public void TestOsuWithoutRedTint() + { + AddStep("Disable red tint", () => Config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false)); + TestOsu(); + AddStep("Enable red tint", () => Config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, true)); + } + protected override void AddCheckSteps() { AddUntilStep("wait for fail", () => Player.HasFailed); diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 2a1c4599d5..242d997dd7 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; using osu.Game.Audio.Effects; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Objects.Drawables; using osuTK; using osuTK.Graphics; @@ -35,7 +36,7 @@ namespace osu.Game.Screens.Play private Container filters; - private Box failFlash; + private Box redFlashLayer; private Track track; @@ -46,6 +47,9 @@ namespace osu.Game.Screens.Play private Sample failSample; + [Resolved] + private OsuConfigManager config { get; set; } + protected override Container Content { get; } = new Container { Anchor = Anchor.Centre, @@ -77,7 +81,7 @@ namespace osu.Game.Screens.Play }, }, Content, - failFlash = new Box + redFlashLayer = new Box { Colour = Color4.Red, RelativeSizeAxes = Axes.Both, @@ -114,7 +118,8 @@ namespace osu.Game.Screens.Play applyToPlayfield(drawableRuleset.Playfield); drawableRuleset.Playfield.HitObjectContainer.FadeOut(duration / 2); - failFlash.FadeOutFromOne(1000); + if (config.Get(OsuSetting.FadePlayfieldWhenHealthLow)) + redFlashLayer.FadeOutFromOne(1000); Content.Masking = true; From 3c4c9ab7a7e6c2c480245e08671990298ed9dc5a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Oct 2021 14:25:51 +0900 Subject: [PATCH 31/79] Move `ICanAcceptFiles` specification to `IModelImporter` --- osu.Game/Database/ArchiveModelManager.cs | 2 +- osu.Game/Database/IModelImporter.cs | 14 +------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 84e33e3f36..9c777d324b 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -30,7 +30,7 @@ namespace osu.Game.Database /// /// The model type. /// The associated file join type. - public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager + public abstract class ArchiveModelManager : IModelManager, IModelFileManager where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : class, INamedFileInfo, new() { diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index 479f33c3b4..5d0a044578 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -13,21 +13,9 @@ namespace osu.Game.Database /// A class which handles importing of associated models to the game store. /// /// The model type. - public interface IModelImporter : IPostNotifications, IPostImports + public interface IModelImporter : IPostNotifications, IPostImports, ICanAcceptFiles where TModel : class { - /// - /// Import one or more items from filesystem . - /// - /// - /// This will be treated as a low priority import if more than one path is specified; use to always import at standard priority. - /// This will post notifications tracking progress. - /// - /// One or more archive locations on disk. - Task Import(params string[] paths); - - Task Import(params ImportTask[] tasks); - Task>> Import(ProgressNotification notification, params ImportTask[] tasks); /// From ad112cbbc52b1d6a9495e0c93548326481b02d30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Oct 2021 14:28:29 +0900 Subject: [PATCH 32/79] Fix intendation in a way it doesn't regress with older `inspectcode` --- osu.Game/Online/API/OAuth.cs | 42 ++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index 0dbc3b02db..1feb3076d1 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -39,17 +39,19 @@ namespace osu.Game.Online.API if (string.IsNullOrEmpty(username)) throw new ArgumentException("Missing username."); if (string.IsNullOrEmpty(password)) throw new ArgumentException("Missing password."); - using (var req = new AccessTokenRequestPassword(username, password) - { - Url = $@"{endpoint}/oauth/token", - Method = HttpMethod.Post, - ClientId = clientId, - ClientSecret = clientSecret - }) + var accessTokenRequest = new AccessTokenRequestPassword(username, password) + { + Url = $@"{endpoint}/oauth/token", + Method = HttpMethod.Post, + ClientId = clientId, + ClientSecret = clientSecret + }; + + using (accessTokenRequest) { try { - req.Perform(); + accessTokenRequest.Perform(); } catch (Exception ex) { @@ -60,7 +62,7 @@ namespace osu.Game.Online.API try { // attempt to decode a displayable error string. - var error = JsonConvert.DeserializeObject(req.GetResponseString() ?? string.Empty); + var error = JsonConvert.DeserializeObject(accessTokenRequest.GetResponseString() ?? string.Empty); if (error != null) throwableException = new APIException(error.UserDisplayableError, ex); } @@ -71,7 +73,7 @@ namespace osu.Game.Online.API throw throwableException; } - Token.Value = req.ResponseObject; + Token.Value = accessTokenRequest.ResponseObject; } } @@ -79,17 +81,19 @@ namespace osu.Game.Online.API { try { - using (var req = new AccessTokenRequestRefresh(refresh) - { - Url = $@"{endpoint}/oauth/token", - Method = HttpMethod.Post, - ClientId = clientId, - ClientSecret = clientSecret - }) + var refreshRequest = new AccessTokenRequestRefresh(refresh) { - req.Perform(); + Url = $@"{endpoint}/oauth/token", + Method = HttpMethod.Post, + ClientId = clientId, + ClientSecret = clientSecret + }; - Token.Value = req.ResponseObject; + using (refreshRequest) + { + refreshRequest.Perform(); + + Token.Value = refreshRequest.ResponseObject; return true; } } From 75bfa705cf3949c0e933ef3b4ba189de339b62b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Oct 2021 14:32:38 +0900 Subject: [PATCH 33/79] Remove unused method for now --- osu.Game/Stores/RealmArchiveModelImporter.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index cc61896ea5..ec454d25fa 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -546,12 +546,5 @@ namespace osu.Game.Stores } public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; - - private string getValidFilename(string filename) - { - foreach (char c in Path.GetInvalidFileNameChars()) - filename = filename.Replace(c, '_'); - return filename; - } } } From 264fa703f2877c52f90f5ffd6ac89225712bd3ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Oct 2021 14:43:48 +0900 Subject: [PATCH 34/79] Remove some forgotten temporary code from `BeatmapImporter` And make the online queue not `dynamic`, at very least. --- osu.Game/Stores/BeatmapImporter.cs | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 33276bff2f..721f507b3d 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -40,29 +40,20 @@ namespace osu.Game.Stores protected override string[] HashableFileTypes => new[] { ".osu" }; - // protected override string ImportFromStablePath => "."; - // - // protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); - // - // protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; - // // protected override bool CheckLocalAvailability(RealmBeatmapSet model, System.Linq.IQueryable items) // => base.CheckLocalAvailability(model, items) || (model.OnlineID != null && items.Any(b => b.OnlineID == model.OnlineID)); - private readonly dynamic? onlineLookupQueue = null; // todo: BeatmapOnlineLookupQueue is private + private readonly BeatmapOnlineLookupQueue? onlineLookupQueue; - public BeatmapImporter(RealmContextFactory contextFactory, Storage storage, bool performOnlineLookups = false) + public BeatmapImporter(RealmContextFactory contextFactory, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null) : base(storage, contextFactory) { - if (performOnlineLookups) - { - // onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); - } + this.onlineLookupQueue = onlineLookupQueue; } protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz"; - protected override async Task Populate(RealmBeatmapSet beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) + protected override Task Populate(RealmBeatmapSet beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { if (archive != null) beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet.Files, realm)); @@ -75,7 +66,10 @@ namespace osu.Game.Stores bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineID > 0); if (onlineLookupQueue != null) - await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); + { + // TODO: this required `BeatmapOnlineLookupQueue` to somehow support new types. + // await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); + } // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineID > 0)) @@ -86,13 +80,12 @@ namespace osu.Game.Stores LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); } } + + return Task.CompletedTask; } protected override void PreImport(RealmBeatmapSet beatmapSet, Realm realm) { - // if (beatmapSet.Beatmaps.Any(b => b.Difficulty == null)) - // throw new InvalidOperationException($"Cannot import {nameof(IBeatmapInfo)} with null {nameof(IBeatmapInfo.Difficulty)}."); - // check if a set already exists with the same online id, delete if it does. if (beatmapSet.OnlineID != null) { From b2f9f8b8da65273df1d74ac64b7ed71913e67350 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Oct 2021 15:06:09 +0900 Subject: [PATCH 35/79] Update logic surrounding removal of previous `OnlineID`s when running a new import --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 2 +- osu.Game/Stores/BeatmapImporter.cs | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index beab9b311b..e2a0e5a79f 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -772,7 +772,7 @@ namespace osu.Game.Tests.Database { IQueryable? resultSets = null; - waitForOrAssert(() => (resultSets = realm.All().Where(s => s.OnlineID == 241526)).Any(), + waitForOrAssert(() => (resultSets = realm.All().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any(), @"BeatmapSet did not import to the database in allocated time.", timeout); // ensure we were stored to beatmap database backing... diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 721f507b3d..5f058872d3 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -86,21 +86,24 @@ namespace osu.Game.Stores protected override void PreImport(RealmBeatmapSet beatmapSet, Realm realm) { - // check if a set already exists with the same online id, delete if it does. + // We are about to import a new beatmap. Before doing so, ensure that no other set shares the online IDs used by the new one. + // Note that this means if the previous beatmap is restored by the user, it will no longer be linked to its online IDs. + // If this is ever an issue, we can consider marking as pending delete but not resetting the IDs (but care will be required for + // beatmaps, which don't have their own `DeletePending` state). + if (beatmapSet.OnlineID != null) { - var existingOnlineId = realm.All().FirstOrDefault(b => b.OnlineID == beatmapSet.OnlineID); + var existingOnlineId = realm.All().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID); if (existingOnlineId != null) { existingOnlineId.DeletePending = true; - - // in order to avoid a unique key constraint, immediately remove the online ID from the previous set. existingOnlineId.OnlineID = null; + foreach (var b in existingOnlineId.Beatmaps) b.OnlineID = null; - LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It has been deleted."); + LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be deleted."); } } } From 2c5ba1d8e2d444795c8f1a986a2a039d18d4c8f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Oct 2021 15:35:51 +0900 Subject: [PATCH 36/79] Change `OnlineID` to non-nullable to allow for indexing in Realm --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- osu.Game/Beatmaps/BeatmapSetInfo.cs | 2 +- osu.Game/Database/IHasOnlineID.cs | 4 +-- osu.Game/Database/RealmContextFactory.cs | 34 +++++++++++++++++++++++- osu.Game/Models/RealmBeatmap.cs | 3 ++- osu.Game/Models/RealmBeatmapSet.cs | 5 ++-- osu.Game/Models/RealmRuleset.cs | 7 ++--- osu.Game/Rulesets/RulesetInfo.cs | 2 +- 8 files changed, 47 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index ac5b5d7a8a..3bcc00f5de 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -178,7 +178,7 @@ namespace osu.Game.Beatmaps #region Implementation of IHasOnlineID - public int? OnlineID => OnlineBeatmapID; + public int OnlineID => OnlineBeatmapID ?? -1; #endregion diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 8b01831b3c..e8c77e792f 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -91,7 +91,7 @@ namespace osu.Game.Beatmaps #region Implementation of IHasOnlineID - public int? OnlineID => OnlineBeatmapSetID; + public int OnlineID => OnlineBeatmapSetID ?? -1; #endregion diff --git a/osu.Game/Database/IHasOnlineID.cs b/osu.Game/Database/IHasOnlineID.cs index c55c461d2d..529c68a8f8 100644 --- a/osu.Game/Database/IHasOnlineID.cs +++ b/osu.Game/Database/IHasOnlineID.cs @@ -8,8 +8,8 @@ namespace osu.Game.Database public interface IHasOnlineID { /// - /// The server-side ID representing this instance, if one exists. + /// The server-side ID representing this instance, if one exists. -1 denotes a missing ID. /// - int? OnlineID { get; } + int OnlineID { get; } } } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 82d51e365e..7403550b1e 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; +using osu.Game.Models; using Realms; #nullable enable @@ -26,7 +28,12 @@ namespace osu.Game.Database /// public readonly string Filename; - private const int schema_version = 6; + /// + /// Version history: + /// 6 First tracked version (~20211018) + /// 7 Changed OnlineID fields to non-nullable to add indexing support (20211018) + /// + private const int schema_version = 7; /// /// Lock object which is held during sections, blocking context creation during blocking periods. @@ -120,6 +127,31 @@ namespace osu.Game.Database private void onMigration(Migration migration, ulong lastSchemaVersion) { + if (lastSchemaVersion < 7) + { + convertOnlineIDs(); + convertOnlineIDs(); + convertOnlineIDs(); + + void convertOnlineIDs() where T : RealmObject + { + var className = typeof(T).Name.Replace(@"Realm", string.Empty); + + var oldItems = migration.OldRealm.DynamicApi.All(className); + var newItems = migration.NewRealm.DynamicApi.All(className); + + int itemCount = newItems.Count(); + + for (int i = 0; i < itemCount; i++) + { + var oldItem = oldItems.ElementAt(i); + var newItem = newItems.ElementAt(i); + + long? nullableOnlineID = oldItem.OnlineID; + newItem.OnlineID = (int)(nullableOnlineID ?? -1); + } + } + } } /// diff --git a/osu.Game/Models/RealmBeatmap.cs b/osu.Game/Models/RealmBeatmap.cs index 5049c1384d..9311425cb7 100644 --- a/osu.Game/Models/RealmBeatmap.cs +++ b/osu.Game/Models/RealmBeatmap.cs @@ -44,7 +44,8 @@ namespace osu.Game.Models [MapTo(nameof(Status))] public int StatusInt { get; set; } - public int? OnlineID { get; set; } + [Indexed] + public int OnlineID { get; set; } = -1; public double Length { get; set; } diff --git a/osu.Game/Models/RealmBeatmapSet.cs b/osu.Game/Models/RealmBeatmapSet.cs index 314ca4494b..d6e56fd61c 100644 --- a/osu.Game/Models/RealmBeatmapSet.cs +++ b/osu.Game/Models/RealmBeatmapSet.cs @@ -20,7 +20,8 @@ namespace osu.Game.Models [PrimaryKey] public Guid ID { get; set; } = Guid.NewGuid(); - public int? OnlineID { get; set; } + [Indexed] + public int OnlineID { get; set; } = -1; public DateTimeOffset DateAdded { get; set; } @@ -62,7 +63,7 @@ namespace osu.Game.Models if (IsManaged && other.IsManaged) return ID == other.ID; - if (OnlineID.HasValue && other.OnlineID.HasValue) + if (OnlineID >= 0 && other.OnlineID >= 0) return OnlineID == other.OnlineID; if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash)) diff --git a/osu.Game/Models/RealmRuleset.cs b/osu.Game/Models/RealmRuleset.cs index 0dcd701ed2..5d70324713 100644 --- a/osu.Game/Models/RealmRuleset.cs +++ b/osu.Game/Models/RealmRuleset.cs @@ -18,7 +18,8 @@ namespace osu.Game.Models [PrimaryKey] public string ShortName { get; set; } = string.Empty; - public int? OnlineID { get; set; } + [Indexed] + public int OnlineID { get; set; } = -1; public string Name { get; set; } = string.Empty; @@ -29,7 +30,7 @@ namespace osu.Game.Models ShortName = shortName; Name = name; InstantiationInfo = instantiationInfo; - OnlineID = onlineID; + OnlineID = onlineID ?? -1; } [UsedImplicitly] @@ -39,7 +40,7 @@ namespace osu.Game.Models public RealmRuleset(int? onlineID, string name, string shortName, bool available) { - OnlineID = onlineID; + OnlineID = onlineID ?? -1; Name = name; ShortName = shortName; Available = available; diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index ca6a083a58..8cd3fa8c63 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets #region Implementation of IHasOnlineID - public int? OnlineID => ID; + public int OnlineID => ID ?? -1; #endregion } From 88a575462cc73e74055450d2a59450bcb653ca3a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Oct 2021 16:11:55 +0900 Subject: [PATCH 37/79] Work around weird null inspection --- osu.Game/Database/RealmContextFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 7403550b1e..078709ad2d 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -147,7 +147,7 @@ namespace osu.Game.Database var oldItem = oldItems.ElementAt(i); var newItem = newItems.ElementAt(i); - long? nullableOnlineID = oldItem.OnlineID; + long? nullableOnlineID = oldItem?.OnlineID; newItem.OnlineID = (int)(nullableOnlineID ?? -1); } } From b3219bb592168188bbe357ab943729714812aaad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Oct 2021 16:10:37 +0900 Subject: [PATCH 38/79] Update usages of `OnlineID` --- .../Database/BeatmapImporterTests.cs | 6 ++--- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 2 +- osu.Game/Stores/BeatmapImporter.cs | 22 +++++++++---------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index e2a0e5a79f..4cdcf507b6 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -458,7 +458,7 @@ namespace osu.Game.Tests.Database realmFactory.Context.Write(() => { foreach (var b in imported.Beatmaps) - b.OnlineID = null; + b.OnlineID = -1; }); deleteBeatmapSet(imported, realmFactory.Context); @@ -509,8 +509,8 @@ namespace osu.Game.Tests.Database Assert.NotNull(imported); Debug.Assert(imported != null); - Assert.AreEqual(null, imported.PerformRead(s => s.Beatmaps[0].OnlineID)); - Assert.AreEqual(null, imported.PerformRead(s => s.Beatmaps[1].OnlineID)); + Assert.AreEqual(-1, imported.PerformRead(s => s.Beatmaps[0].OnlineID)); + Assert.AreEqual(-1, imported.PerformRead(s => s.Beatmaps[1].OnlineID)); }); } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 562cbfabf0..0509a9db47 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps /// Handles general operations related to global beatmap management. /// [ExcludeFromDynamicCompile] - public class BeatmapManager : IModelDownloader, IModelManager, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache, IDisposable + public class BeatmapManager : IModelDownloader, IModelManager, IModelFileManager, IWorkingBeatmapCache, IDisposable { private readonly BeatmapModelManager beatmapModelManager; private readonly BeatmapModelDownloader beatmapModelDownloader; diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 8494cdcd22..a9791fba7e 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -25,7 +25,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring { - public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles + public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader { private readonly Scheduler scheduler; private readonly Func difficulties; diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 5f058872d3..4ff09e6cc5 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -41,7 +41,7 @@ namespace osu.Game.Stores protected override string[] HashableFileTypes => new[] { ".osu" }; // protected override bool CheckLocalAvailability(RealmBeatmapSet model, System.Linq.IQueryable items) - // => base.CheckLocalAvailability(model, items) || (model.OnlineID != null && items.Any(b => b.OnlineID == model.OnlineID)); + // => base.CheckLocalAvailability(model, items) || (model.OnlineID > -1)); private readonly BeatmapOnlineLookupQueue? onlineLookupQueue; @@ -74,9 +74,9 @@ namespace osu.Game.Stores // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineID > 0)) { - if (beatmapSet.OnlineID != null) + if (beatmapSet.OnlineID > -1) { - beatmapSet.OnlineID = null; + beatmapSet.OnlineID = -1; LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); } } @@ -91,17 +91,17 @@ namespace osu.Game.Stores // If this is ever an issue, we can consider marking as pending delete but not resetting the IDs (but care will be required for // beatmaps, which don't have their own `DeletePending` state). - if (beatmapSet.OnlineID != null) + if (beatmapSet.OnlineID > -1) { var existingOnlineId = realm.All().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID); if (existingOnlineId != null) { existingOnlineId.DeletePending = true; - existingOnlineId.OnlineID = null; + existingOnlineId.OnlineID = -1; foreach (var b in existingOnlineId.Beatmaps) - b.OnlineID = null; + b.OnlineID = -1; LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be deleted."); } @@ -110,7 +110,7 @@ namespace osu.Game.Stores private void validateOnlineIds(RealmBeatmapSet beatmapSet, Realm realm) { - var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineID.HasValue).Select(b => b.OnlineID).ToList(); + var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineID > -1).Select(b => b.OnlineID).ToList(); // ensure all IDs are unique if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1)) @@ -140,7 +140,7 @@ namespace osu.Game.Stores } } - void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineID = null); + void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineID = -1); } protected override bool CanSkipImport(RealmBeatmapSet existing, RealmBeatmapSet import) @@ -148,7 +148,7 @@ namespace osu.Game.Stores if (!base.CanSkipImport(existing, import)) return false; - return existing.Beatmaps.Any(b => b.OnlineID != null); + return existing.Beatmaps.Any(b => b.OnlineID > -1); } protected override bool CanReuseExisting(RealmBeatmapSet existing, RealmBeatmapSet import) @@ -182,7 +182,7 @@ namespace osu.Game.Stores return new RealmBeatmapSet { - OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID, + OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID ?? -1, // Metadata = beatmap.Metadata, DateAdded = DateTimeOffset.UtcNow }; @@ -250,7 +250,7 @@ namespace osu.Game.Stores { Hash = hash, DifficultyName = decodedInfo.Version, - OnlineID = decodedInfo.OnlineBeatmapID, + OnlineID = decodedInfo.OnlineBeatmapID ?? -1, AudioLeadIn = decodedInfo.AudioLeadIn, StackLeniency = decodedInfo.StackLeniency, SpecialStyle = decodedInfo.SpecialStyle, From 830f49bca693c1d6c774dd4237cdba58bb337081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Oct 2021 19:43:48 +0200 Subject: [PATCH 39/79] Remove doubled-up opacity specification --- osu.Game/Overlays/RestoreDefaultValueButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/RestoreDefaultValueButton.cs b/osu.Game/Overlays/RestoreDefaultValueButton.cs index de600e8172..afc4146199 100644 --- a/osu.Game/Overlays/RestoreDefaultValueButton.cs +++ b/osu.Game/Overlays/RestoreDefaultValueButton.cs @@ -115,7 +115,7 @@ namespace osu.Game.Overlays Background.FadeColour(colours.Lime3, fade_duration, Easing.OutQuint); Content.TweenEdgeEffectTo(new EdgeEffectParameters { - Colour = colours.Lime3.Opacity(0.4f).Opacity(0.1f), + Colour = colours.Lime3.Opacity(0.1f), Radius = 2, Type = EdgeEffectType.Glow }, fade_duration, Easing.OutQuint); From 6c3637a62a993d4cbcfe41eee99cd28f89c99929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Oct 2021 20:36:47 +0200 Subject: [PATCH 40/79] Remove grid usage in `KeyBindingRow` --- .../Settings/Sections/Input/KeyBindingRow.cs | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index c2667fbdac..f44f02d0ed 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -82,32 +82,29 @@ namespace osu.Game.Overlays.Settings.Sections.Input { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + Padding = new MarginPadding { Right = SettingsPanel.CONTENT_MARGINS }; - InternalChild = new GridContainer + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] + new Container { - new Dimension(GridSizeMode.Absolute, SettingsPanel.CONTENT_MARGINS), - new Dimension(), - new Dimension(GridSizeMode.Absolute, SettingsPanel.CONTENT_MARGINS), - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] + RelativeSizeAxes = Axes.Y, + Width = SettingsPanel.CONTENT_MARGINS, + Child = new RestoreDefaultValueButton + { + Current = isDefault, + Action = RestoreDefaults, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS }, + Children = new Drawable[] { - new RestoreDefaultValueButton - { - Current = isDefault, - Action = RestoreDefaults, - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, content = new Container { RelativeSizeAxes = Axes.X, @@ -154,10 +151,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, } } - }, - new HoverClickSounds() + } } - } + }, + new HoverClickSounds() }; foreach (var b in bindings) From 88a1b31fae9538a0e38ffc3aef9e9167e90a3120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Oct 2021 21:01:43 +0200 Subject: [PATCH 41/79] Remove grid usage in `SettingsItem` --- .../Visual/Settings/TestSceneSettingsItem.cs | 21 +++++--- osu.Game/Overlays/Settings/SettingsItem.cs | 54 ++++++------------- osu.Game/Overlays/Settings/SettingsTextBox.cs | 1 - 3 files changed, 32 insertions(+), 44 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs index 9c3940af4c..83265e13ad 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs @@ -4,8 +4,10 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; -using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; using osu.Game.Overlays; @@ -47,8 +49,8 @@ namespace osu.Game.Tests.Visual.Settings public void TestSetAndClearLabelText() { SettingsTextBox textBox = null; - GridContainer settingsItemGrid = null; RestoreDefaultValueButton restoreDefaultValueButton = null; + OsuTextBox control = null; AddStep("create settings item", () => { @@ -64,18 +66,25 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("wait for loaded", () => textBox.IsLoaded); AddStep("retrieve components", () => { - settingsItemGrid = textBox.ChildrenOfType().Single(); restoreDefaultValueButton = textBox.ChildrenOfType>().Single(); + control = textBox.ChildrenOfType().Single(); }); AddStep("set non-default value", () => restoreDefaultValueButton.Current.Value = "non-default"); - AddAssert("default value button next to control", () => settingsItemGrid.Content[1][0] == restoreDefaultValueButton); + AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); AddStep("set label", () => textBox.LabelText = "label text"); - AddAssert("default value button next to label", () => settingsItemGrid.Content[0][0] == restoreDefaultValueButton); + AddAssert("default value button centre aligned to label size", () => + { + var label = textBox.ChildrenOfType().Single(spriteText => spriteText.Text == "label text"); + return Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, label.DrawHeight, 1); + }); AddStep("clear label", () => textBox.LabelText = default); - AddAssert("default value button next to control", () => settingsItemGrid.Content[1][0] == restoreDefaultValueButton); + AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); + + AddStep("set warning text", () => textBox.WarningText = "This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator..."); + AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); } /// diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 7895f6386b..91998f07d8 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -32,14 +31,11 @@ namespace osu.Game.Overlays.Settings protected readonly FillFlowContainer FlowContent; private SpriteText labelText; - private readonly GridContainer gridContainer; - - [CanBeNull] - private RestoreDefaultValueButton defaultValueButton; private OsuTextFlowContainer warningText; public bool ShowsDefaultIndicator = true; + private readonly Container defaultValueIndicatorContainer; public LocalisableString TooltipText { get; set; } @@ -54,7 +50,7 @@ namespace osu.Game.Overlays.Settings if (labelText == null) { // construct lazily for cases where the label is not needed (may be provided by the Control). - gridContainer.Content[0][1] = labelText = new OsuSpriteText(); + FlowContent.Insert(-1, labelText = new OsuSpriteText()); updateDisabled(); } @@ -113,32 +109,19 @@ namespace osu.Game.Overlays.Settings AutoSizeAxes = Axes.Y; Padding = new MarginPadding { Right = SettingsPanel.CONTENT_MARGINS }; - FlowContent = new FillFlowContainer + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0, 10), - Child = Control = CreateControl(), - }; - - InternalChild = gridContainer = new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - RowDimensions = new[] + defaultValueIndicatorContainer = new Container { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize) + Width = SettingsPanel.CONTENT_MARGINS, }, - ColumnDimensions = new[] + FlowContent = new FillFlowContainer { - new Dimension(GridSizeMode.Absolute, SettingsPanel.CONTENT_MARGINS), - new Dimension() - }, - Content = new[] - { - new Drawable[2], - new Drawable[] { null, FlowContent } + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS }, + Spacing = new Vector2(0, 10), + Child = Control = CreateControl(), } }; @@ -157,26 +140,23 @@ namespace osu.Game.Overlays.Settings // intentionally done before LoadComplete to avoid overhead. if (ShowsDefaultIndicator) { - defaultValueButton = new RestoreDefaultValueButton + defaultValueIndicatorContainer.Add(new RestoreDefaultValueButton { Current = controlWithCurrent.Current, Anchor = Anchor.Centre, Origin = Anchor.Centre - }; + }); updateLayout(); } } private void updateLayout() { - bool hasLabel = !string.IsNullOrEmpty(labelText?.Text.ToString()); + bool hasLabel = labelText != null && !string.IsNullOrEmpty(labelText.Text.ToString()); - gridContainer.Content[0][0] = null; - gridContainer.Content[1][0] = null; - - gridContainer.Content[hasLabel ? 0 : 1][0] = defaultValueButton; - - FlowContent.Margin = new MarginPadding { Top = hasLabel ? 10 : 0 }; + // if the settings item is providing a label, the default value indicator should be centred vertically to the left of the label. + // otherwise, it should be centred vertically to the left of the main control of the settings item. + defaultValueIndicatorContainer.Height = hasLabel ? labelText.DrawHeight : Control.DrawHeight; } private void updateDisabled() diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs index 68562802cf..a724003183 100644 --- a/osu.Game/Overlays/Settings/SettingsTextBox.cs +++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs @@ -11,7 +11,6 @@ namespace osu.Game.Overlays.Settings { protected override Drawable CreateControl() => new OutlinedTextBox { - Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, CommitOnFocusLost = true }; From 6d9d85685f73cadaf2619c43af3ff33da6e0a2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Oct 2021 23:30:38 +0200 Subject: [PATCH 42/79] Fix settings item having zero height --- osu.Game/Overlays/Settings/SettingsItem.cs | 11 ++++++++--- .../Screens/Play/PlayerSettings/PlayerSliderBar.cs | 1 - 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 91998f07d8..b593dea576 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -115,13 +115,18 @@ namespace osu.Game.Overlays.Settings { Width = SettingsPanel.CONTENT_MARGINS, }, - FlowContent = new FillFlowContainer + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS }, - Spacing = new Vector2(0, 10), - Child = Control = CreateControl(), + Child = FlowContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 10), + Child = Control = CreateControl(), + } } }; diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs index 216e46d429..9903a74043 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs @@ -17,7 +17,6 @@ namespace osu.Game.Screens.Play.PlayerSettings protected override Drawable CreateControl() => new Sliderbar { - Margin = new MarginPadding { Top = 5, Bottom = 5 }, RelativeSizeAxes = Axes.X }; From 8db2fc439d53823fdb0bc67760aaaaa236574e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 Oct 2021 23:45:06 +0200 Subject: [PATCH 43/79] Change ruleset ID assert in player to null-check --- osu.Game/Screens/Play/Player.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c8b946b7f1..1381493fdf 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -218,12 +217,11 @@ namespace osu.Game.Screens.Play Score = CreateScore(playableBeatmap); - Debug.Assert(ruleset.RulesetInfo.ID != null); - // ensure the score is in a consistent state with the current player. Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; - Score.ScoreInfo.RulesetID = ruleset.RulesetInfo.ID.Value; + if (ruleset.RulesetInfo.ID != null) + Score.ScoreInfo.RulesetID = ruleset.RulesetInfo.ID.Value; Score.ScoreInfo.Mods = gameplayMods; dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score)); From 95ed9a431ce5b138727dac484674ec9ae6d05f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Oct 2021 00:06:07 +0200 Subject: [PATCH 44/79] Add test coverage for no submission for ruleset with null ID --- .../Visual/Gameplay/TestScenePlayerScoreSubmission.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index d7edb33c42..bf864f844c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -227,12 +227,13 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("ensure no submission", () => Player.SubmittedScore == null); } - [Test] - public void TestNoSubmissionOnCustomRuleset() + [TestCase(null)] + [TestCase(10)] + public void TestNoSubmissionOnCustomRuleset(int? rulesetId) { prepareTokenResponse(true); - createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { ID = 10 } }); + createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { ID = rulesetId } }); AddUntilStep("wait for token request", () => Player.TokenCreationRequested); From ff2eae45974d39ce26e75617bd976e224aa719f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Oct 2021 11:37:46 +0900 Subject: [PATCH 45/79] Rename confusing variable --- osu.Game/Beatmaps/BeatmapModelManager.cs | 10 +++++----- osu.Game/Stores/BeatmapImporter.cs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 76019a15ae..16cf6193f9 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -123,15 +123,15 @@ namespace osu.Game.Beatmaps // check if a set already exists with the same online id, delete if it does. if (beatmapSet.OnlineBeatmapSetID != null) { - var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID); + var existingSetWithSameOnlineID = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID); - if (existingOnlineId != null) + if (existingSetWithSameOnlineID != null) { - Delete(existingOnlineId); + Delete(existingSetWithSameOnlineID); // in order to avoid a unique key constraint, immediately remove the online ID from the previous set. - existingOnlineId.OnlineBeatmapSetID = null; - foreach (var b in existingOnlineId.Beatmaps) + existingSetWithSameOnlineID.OnlineBeatmapSetID = null; + foreach (var b in existingSetWithSameOnlineID.Beatmaps) b.OnlineBeatmapID = null; LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted."); diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 4ff09e6cc5..dc43fda09a 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -93,14 +93,14 @@ namespace osu.Game.Stores if (beatmapSet.OnlineID > -1) { - var existingOnlineId = realm.All().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID); + var existingSetWithSameOnlineID = realm.All().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID); - if (existingOnlineId != null) + if (existingSetWithSameOnlineID != null) { - existingOnlineId.DeletePending = true; - existingOnlineId.OnlineID = -1; + existingSetWithSameOnlineID.DeletePending = true; + existingSetWithSameOnlineID.OnlineID = -1; - foreach (var b in existingOnlineId.Beatmaps) + foreach (var b in existingSetWithSameOnlineID.Beatmaps) b.OnlineID = -1; LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be deleted."); From e7b280fc81899fa60d9f0fc7ef169beb1db5d0d7 Mon Sep 17 00:00:00 2001 From: sh0ckR6 Date: Mon, 18 Oct 2021 23:12:33 -0400 Subject: [PATCH 46/79] Comment out RankDisplay Commented out as it may be revisited at a later time. Also currently unused. --- osu.Game/Screens/Play/Break/BreakInfo.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Break/BreakInfo.cs b/osu.Game/Screens/Play/Break/BreakInfo.cs index 6e129b20ea..6349ebd9a7 100644 --- a/osu.Game/Screens/Play/Break/BreakInfo.cs +++ b/osu.Game/Screens/Play/Break/BreakInfo.cs @@ -13,7 +13,9 @@ namespace osu.Game.Screens.Play.Break public class BreakInfo : Container { public PercentageBreakInfoLine AccuracyDisplay; - public BreakInfoLine RankDisplay; + + // Currently unused but may be revisited in a future design update (see https://github.com/ppy/osu/discussions/15185) + // public BreakInfoLine RankDisplay; public BreakInfoLine GradeDisplay; public BreakInfo() @@ -41,7 +43,9 @@ namespace osu.Game.Screens.Play.Break Children = new Drawable[] { AccuracyDisplay = new PercentageBreakInfoLine("Accuracy"), - RankDisplay = new BreakInfoLine("Rank"), + + // See https://github.com/ppy/osu/discussions/15185 + // RankDisplay = new BreakInfoLine("Rank"), GradeDisplay = new BreakInfoLine("Grade"), }, } From d3ab45084d52566d5a06e1d57c4864e028105ed1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Oct 2021 14:19:58 +0900 Subject: [PATCH 47/79] Fix realm migration potentially failing from older releases --- osu.Game/Database/RealmContextFactory.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 078709ad2d..b5c44927ca 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -137,6 +137,11 @@ namespace osu.Game.Database { var className = typeof(T).Name.Replace(@"Realm", string.Empty); + // version was not bumped when the beatmap/ruleset models were added + // therefore we must manually check for their presence to avoid throwing on the `DynamicApi` calls. + if (!migration.OldRealm.Schema.TryFindObjectSchema(className, out _)) + return; + var oldItems = migration.OldRealm.DynamicApi.All(className); var newItems = migration.NewRealm.DynamicApi.All(className); From 192cfe871705306d6c73dc77a3e9a01f02cf906f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Oct 2021 16:42:07 +0900 Subject: [PATCH 48/79] Replace unnecessary `ToString` call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index b763d39c3b..cba24eb8e6 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -196,7 +196,7 @@ namespace osu.Game.Configuration string skinName = LookupSkinName(m) ?? string.Empty; return new SettingDescription(skinName, SkinSettingsStrings.SkinSectionHeader, skinName, $"{GlobalActionKeyBindingStrings.RandomSkin}: {LookupKeyBindings(GlobalAction.RandomSkin)}"); }), - new TrackedSetting(OsuSetting.UIScale, m => new SettingDescription(m, GraphicsSettingsStrings.UIScaling, $"{m.ToString("N2")}x")), // TODO: implement lookup for framework platform key bindings + new TrackedSetting(OsuSetting.UIScale, m => new SettingDescription(m, GraphicsSettingsStrings.UIScaling, $"{m:N2}x")), // TODO: implement lookup for framework platform key bindings }; } From 61670a70b6df88bcc8cf9f1d85d8025dc9506d97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Oct 2021 17:00:51 +0900 Subject: [PATCH 49/79] Tidy up tracked settings code syntax and fix remaining issue --- .../ManiaRulesetConfigManager.cs | 7 ++- osu.Game/Configuration/OsuConfigManager.cs | 45 ++++++++++++++++--- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index ac8168dfc9..44a501f748 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -28,7 +28,12 @@ namespace osu.Game.Rulesets.Mania.Configuration public override TrackedSettings CreateTrackedSettings() => new TrackedSettings { new TrackedSetting(ManiaRulesetSetting.ScrollTime, - v => new SettingDescription(v, "Scroll Speed", $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)} ({v}ms)")) + v => new SettingDescription( + rawValue: v, + name: "Scroll Speed", + value: $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)} ({v}ms)" + ) + ) }; } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index cba24eb8e6..0790c62499 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -188,15 +188,46 @@ namespace osu.Game.Configuration return new TrackedSettings { - new TrackedSetting(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, GlobalActionKeyBindingStrings.ToggleGameplayMouseButtons, v ? CommonStrings.Disabled.ToLower() : CommonStrings.Enabled.ToLower(), LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons))), - new TrackedSetting(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, GameplaySettingsStrings.HUDVisibilityMode, m.GetLocalisableDescription(), $"{GlobalActionKeyBindingStrings.ToggleInGameInterface}: {LookupKeyBindings(GlobalAction.ToggleInGameInterface)} {GlobalActionKeyBindingStrings.HoldForHUD}: {LookupKeyBindings(GlobalAction.HoldForHUD)}")), - new TrackedSetting(OsuSetting.Scaling, m => new SettingDescription(m, GraphicsSettingsStrings.ScreenScaling, m.GetLocalisableDescription())), - new TrackedSetting(OsuSetting.Skin, m => + new TrackedSetting(OsuSetting.MouseDisableButtons, disabledState => new SettingDescription( + rawValue: !disabledState, + name: GlobalActionKeyBindingStrings.ToggleGameplayMouseButtons, + value: disabledState ? CommonStrings.Disabled.ToLower() : CommonStrings.Enabled.ToLower(), + shortcut: LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons)) + ), + new TrackedSetting(OsuSetting.HUDVisibilityMode, mode => new SettingDescription( + rawValue: mode, + name: GameplaySettingsStrings.HUDVisibilityMode, + value: mode.GetLocalisableDescription(), + shortcut: new TranslatableString(@"_", @"{0}: {1} {2}: {3}", + GlobalActionKeyBindingStrings.ToggleInGameInterface, + LookupKeyBindings(GlobalAction.ToggleInGameInterface), + GlobalActionKeyBindingStrings.HoldForHUD, + LookupKeyBindings(GlobalAction.HoldForHUD))) + ), + new TrackedSetting(OsuSetting.Scaling, scalingMode => new SettingDescription( + rawValue: scalingMode, + name: GraphicsSettingsStrings.ScreenScaling, + value: scalingMode.GetLocalisableDescription() + ) + ), + new TrackedSetting(OsuSetting.Skin, skin => { - string skinName = LookupSkinName(m) ?? string.Empty; - return new SettingDescription(skinName, SkinSettingsStrings.SkinSectionHeader, skinName, $"{GlobalActionKeyBindingStrings.RandomSkin}: {LookupKeyBindings(GlobalAction.RandomSkin)}"); + string skinName = LookupSkinName(skin) ?? string.Empty; + + return new SettingDescription( + rawValue: skinName, + name: SkinSettingsStrings.SkinSectionHeader, + value: skinName, + shortcut: $"{GlobalActionKeyBindingStrings.RandomSkin}: {LookupKeyBindings(GlobalAction.RandomSkin)}" + ); }), - new TrackedSetting(OsuSetting.UIScale, m => new SettingDescription(m, GraphicsSettingsStrings.UIScaling, $"{m:N2}x")), // TODO: implement lookup for framework platform key bindings + new TrackedSetting(OsuSetting.UIScale, scale => new SettingDescription( + rawValue: scale, + name: GraphicsSettingsStrings.UIScaling, + value: $"{scale:N2}x" + // TODO: implement lookup for framework platform key bindings + ) + ), }; } From 8672b3325a776726a8533d6b134aae83182ef5d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Oct 2021 17:22:50 +0900 Subject: [PATCH 50/79] Fix a couple more weird variable names --- .../Configuration/ManiaRulesetConfigManager.cs | 6 +++--- osu.Game/Configuration/OsuConfigManager.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index 44a501f748..8e09a01469 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -28,10 +28,10 @@ namespace osu.Game.Rulesets.Mania.Configuration public override TrackedSettings CreateTrackedSettings() => new TrackedSettings { new TrackedSetting(ManiaRulesetSetting.ScrollTime, - v => new SettingDescription( - rawValue: v, + scrollTime => new SettingDescription( + rawValue: scrollTime, name: "Scroll Speed", - value: $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)} ({v}ms)" + value: $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)} ({scrollTime}ms)" ) ) }; diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 0790c62499..acb4a9ca02 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -194,10 +194,10 @@ namespace osu.Game.Configuration value: disabledState ? CommonStrings.Disabled.ToLower() : CommonStrings.Enabled.ToLower(), shortcut: LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons)) ), - new TrackedSetting(OsuSetting.HUDVisibilityMode, mode => new SettingDescription( - rawValue: mode, + new TrackedSetting(OsuSetting.HUDVisibilityMode, visibilityMode => new SettingDescription( + rawValue: visibilityMode, name: GameplaySettingsStrings.HUDVisibilityMode, - value: mode.GetLocalisableDescription(), + value: visibilityMode.GetLocalisableDescription(), shortcut: new TranslatableString(@"_", @"{0}: {1} {2}: {3}", GlobalActionKeyBindingStrings.ToggleInGameInterface, LookupKeyBindings(GlobalAction.ToggleInGameInterface), From 6c18d46443e178e9519380edb55e43b9c847b6a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Oct 2021 18:12:02 +0200 Subject: [PATCH 51/79] Add test scene for demonstrating `OsuDropdown` appearance --- .../UserInterface/TestSceneOsuDropdown.cs | 20 ++++++ .../UserInterface/ThemeComparisonTestScene.cs | 69 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs create mode 100644 osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs new file mode 100644 index 0000000000..9e77fcf675 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneOsuDropdown : ThemeComparisonTestScene + { + protected override Drawable CreateContent() => + new OsuEnumDropdown + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 150 + }; + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs new file mode 100644 index 0000000000..f8b9e8223b --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.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 System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public abstract class ThemeComparisonTestScene : OsuGridTestScene + { + protected ThemeComparisonTestScene() + : base(1, 2) + { + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Cell(0, 0).AddRange(new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.GreySeafoam + }, + CreateContent() + }); + } + + private void createThemedContent(OverlayColourScheme colourScheme) + { + var colourProvider = new OverlayColourProvider(colourScheme); + + Cell(0, 1).Clear(); + Cell(0, 1).Add(new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(OverlayColourProvider), colourProvider) + }, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4 + }, + CreateContent() + } + }); + } + + protected abstract Drawable CreateContent(); + + [Test] + public void TestAllColourSchemes() + { + foreach (var scheme in Enum.GetValues(typeof(OverlayColourScheme)).Cast()) + AddStep($"set {scheme} scheme", () => createThemedContent(scheme)); + } + } +} From ef03787fe09ad6707675e4793a05b6135bb11079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Oct 2021 18:33:17 +0200 Subject: [PATCH 52/79] Split dropdown accent colour into hover and selection colours --- .../Collections/CollectionFilterDropdown.cs | 6 +- .../Graphics/UserInterface/OsuDropdown.cs | 113 +++++++----------- .../Graphics/UserInterface/OsuTabDropdown.cs | 35 +++++- osu.Game/Overlays/Login/UserDropdown.cs | 8 +- osu.Game/Overlays/Music/CollectionDropdown.cs | 8 +- .../Overlays/Rankings/SpotlightSelector.cs | 11 +- 6 files changed, 90 insertions(+), 91 deletions(-) diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index 1eceb56e33..7067f82fd3 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -181,7 +181,11 @@ namespace osu.Game.Collections MaxHeight = 200; } - protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item); + protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item) + { + BackgroundColourHover = HoverColour, + BackgroundColourSelected = SelectionColour + }; } protected class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 5831d9ab1f..021f36d4fd 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -21,44 +21,17 @@ using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface { - public class OsuDropdown : Dropdown, IHasAccentColour + public class OsuDropdown : Dropdown { private const float corner_radius = 5; - private Color4 accentColour; - - public Color4 AccentColour - { - get => accentColour; - set - { - accentColour = value; - updateAccentColour(); - } - } - - [BackgroundDependencyLoader(true)] - private void load(OverlayColourProvider? colourProvider, OsuColour colours) - { - if (accentColour == default) - accentColour = colourProvider?.Light4 ?? colours.PinkDarker; - updateAccentColour(); - } - - private void updateAccentColour() - { - if (Header is IHasAccentColour header) header.AccentColour = accentColour; - - if (Menu is IHasAccentColour menu) menu.AccentColour = accentColour; - } - protected override DropdownHeader CreateHeader() => new OsuDropdownHeader(); protected override DropdownMenu CreateMenu() => new OsuDropdownMenu(); #region OsuDropdownMenu - protected class OsuDropdownMenu : DropdownMenu, IHasAccentColour + protected class OsuDropdownMenu : DropdownMenu { public override bool HandleNonPositionalInput => State == MenuState.Open; @@ -78,9 +51,11 @@ namespace osu.Game.Graphics.UserInterface } [BackgroundDependencyLoader(true)] - private void load(OverlayColourProvider? colourProvider, AudioManager audio) + private void load(OverlayColourProvider? colourProvider, OsuColour colours, AudioManager audio) { BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); + HoverColour = colourProvider?.Highlight1 ?? colours.PinkDarker; + SelectionColour = colourProvider?.Light4 ?? colours.PinkDarker.Opacity(0.5f); sampleOpen = audio.Samples.Get(@"UI/dropdown-open"); sampleClose = audio.Samples.Get(@"UI/dropdown-close"); @@ -121,57 +96,77 @@ namespace osu.Game.Graphics.UserInterface } } - private Color4 accentColour; + private Color4 hoverColour; - public Color4 AccentColour + public Color4 HoverColour { - get => accentColour; + get => hoverColour; set { - accentColour = value; - foreach (var c in Children.OfType()) - c.AccentColour = value; + hoverColour = value; + foreach (var c in Children.OfType()) + c.BackgroundColourHover = value; + } + } + + private Color4 selectionColour; + + public Color4 SelectionColour + { + get => selectionColour; + set + { + selectionColour = value; + foreach (var c in Children.OfType()) + c.BackgroundColourSelected = value; } } protected override Menu CreateSubMenu() => new OsuMenu(Direction.Vertical); - protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuDropdownMenuItem(item) { AccentColour = accentColour }; + protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuDropdownMenuItem(item) + { + BackgroundColourHover = HoverColour, + BackgroundColourSelected = SelectionColour + }; protected override ScrollContainer CreateScrollContainer(Direction direction) => new OsuScrollContainer(direction); #region DrawableOsuDropdownMenuItem - public class DrawableOsuDropdownMenuItem : DrawableDropdownMenuItem, IHasAccentColour + public class DrawableOsuDropdownMenuItem : DrawableDropdownMenuItem { // IsHovered is used public override bool HandlePositionalInput => true; - private Color4? accentColour; - - public Color4 AccentColour + public new Color4 BackgroundColourHover { - get => accentColour ?? nonAccentSelectedColour; + get => base.BackgroundColourHover; set { - accentColour = value; + base.BackgroundColourHover = value; + updateColours(); + } + } + + public new Color4 BackgroundColourSelected + { + get => base.BackgroundColourSelected; + set + { + base.BackgroundColourSelected = value; updateColours(); } } private void updateColours() { - BackgroundColourHover = accentColour ?? nonAccentHoverColour; - BackgroundColourSelected = accentColour ?? nonAccentSelectedColour; BackgroundColour = BackgroundColourHover.Opacity(0); UpdateBackgroundColour(); UpdateForegroundColour(); } - private Color4 nonAccentHoverColour; - private Color4 nonAccentSelectedColour; - public DrawableOsuDropdownMenuItem(MenuItem item) : base(item) { @@ -182,12 +177,8 @@ namespace osu.Game.Graphics.UserInterface } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - nonAccentHoverColour = colours.PinkDarker; - nonAccentSelectedColour = Color4.Black.Opacity(0.5f); - updateColours(); - AddInternal(new HoverSounds()); } @@ -290,7 +281,7 @@ namespace osu.Game.Graphics.UserInterface #endregion - public class OsuDropdownHeader : DropdownHeader, IHasAccentColour + public class OsuDropdownHeader : DropdownHeader { protected readonly SpriteText Text; @@ -302,18 +293,6 @@ namespace osu.Game.Graphics.UserInterface protected readonly SpriteIcon Icon; - private Color4 accentColour; - - public virtual Color4 AccentColour - { - get => accentColour; - set - { - accentColour = value; - BackgroundColourHover = accentColour; - } - } - public OsuDropdownHeader() { Foreground.Padding = new MarginPadding(10); @@ -365,7 +344,7 @@ namespace osu.Game.Graphics.UserInterface private void load(OverlayColourProvider? colourProvider, OsuColour colours) { BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); - BackgroundColourHover = colourProvider?.Light4 ?? colours.PinkDarker; + BackgroundColourHover = colourProvider?.Highlight1 ?? colours.PinkDarker; } } } diff --git a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs index 24b9ca8d90..7f21f69065 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs @@ -11,8 +11,28 @@ using osu.Framework.Input.Events; namespace osu.Game.Graphics.UserInterface { - public class OsuTabDropdown : OsuDropdown + public class OsuTabDropdown : OsuDropdown, IHasAccentColour { + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + accentColour = value; + + if (Menu is OsuDropdownMenu dropdownMenu) + { + dropdownMenu.HoverColour = value; + dropdownMenu.SelectionColour = value.Opacity(0.5f); + } + + if (Header is OsuTabDropdownHeader tabDropdownHeader) + tabDropdownHeader.AccentColour = value; + } + } + public OsuTabDropdown() { RelativeSizeAxes = Axes.X; @@ -37,7 +57,7 @@ namespace osu.Game.Graphics.UserInterface MaxHeight = 400; } - protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuTabDropdownMenuItem(item) { AccentColour = AccentColour }; + protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuTabDropdownMenuItem(item); private class DrawableOsuTabDropdownMenuItem : DrawableOsuDropdownMenuItem { @@ -49,14 +69,17 @@ namespace osu.Game.Graphics.UserInterface } } - protected class OsuTabDropdownHeader : OsuDropdownHeader + protected class OsuTabDropdownHeader : OsuDropdownHeader, IHasAccentColour { - public override Color4 AccentColour + private Color4 accentColour; + + public Color4 AccentColour { - get => base.AccentColour; + get => accentColour; set { - base.AccentColour = value; + accentColour = value; + BackgroundColourHover = value; Foreground.Colour = value; } } diff --git a/osu.Game/Overlays/Login/UserDropdown.cs b/osu.Game/Overlays/Login/UserDropdown.cs index ac4e7f8eda..d7c47351ec 100644 --- a/osu.Game/Overlays/Login/UserDropdown.cs +++ b/osu.Game/Overlays/Login/UserDropdown.cs @@ -29,12 +29,6 @@ namespace osu.Game.Overlays.Login } } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - AccentColour = colours.Gray5; - } - protected class UserDropdownMenu : OsuDropdownMenu { public UserDropdownMenu() @@ -56,6 +50,7 @@ namespace osu.Game.Overlays.Login private void load(OsuColour colours) { BackgroundColour = colours.Gray3; + HoverColour = SelectionColour = colours.Gray5; } protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableUserDropdownMenuItem(item); @@ -118,6 +113,7 @@ namespace osu.Game.Overlays.Login private void load(OsuColour colours) { BackgroundColour = colours.Gray3; + BackgroundColourHover = colours.Gray5; } } } diff --git a/osu.Game/Overlays/Music/CollectionDropdown.cs b/osu.Game/Overlays/Music/CollectionDropdown.cs index ed0ebf696b..d4686dee10 100644 --- a/osu.Game/Overlays/Music/CollectionDropdown.cs +++ b/osu.Game/Overlays/Music/CollectionDropdown.cs @@ -19,12 +19,6 @@ namespace osu.Game.Overlays.Music { protected override bool ShowManageCollectionsItem => false; - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - AccentColour = colours.Gray6; - } - protected override CollectionDropdownHeader CreateCollectionHeader() => new CollectionsHeader(); protected override CollectionDropdownMenu CreateCollectionMenu() => new CollectionsMenu(); @@ -41,6 +35,7 @@ namespace osu.Game.Overlays.Music private void load(OsuColour colours) { BackgroundColour = colours.Gray4; + HoverColour = SelectionColour = colours.Gray6; } } @@ -50,6 +45,7 @@ namespace osu.Game.Overlays.Music private void load(OsuColour colours) { BackgroundColour = colours.Gray4; + BackgroundColourHover = colours.Gray6; } public CollectionsHeader() diff --git a/osu.Game/Overlays/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs index 0f071883ca..dfa45cc543 100644 --- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs +++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs @@ -175,18 +175,18 @@ namespace osu.Game.Overlays.Rankings private class SpotlightsDropdown : OsuDropdown { - private DropdownMenu menu; + private OsuDropdownMenu menu; - protected override DropdownMenu CreateMenu() => menu = base.CreateMenu().With(m => m.MaxHeight = 400); + protected override DropdownMenu CreateMenu() => menu = (OsuDropdownMenu)base.CreateMenu().With(m => m.MaxHeight = 400); protected override DropdownHeader CreateHeader() => new SpotlightsDropdownHeader(); [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - // osu-web adds a 0.6 opacity container on top of the 0.5 base one when hovering, 0.8 on a single container here matches the resulting colour - AccentColour = colourProvider.Background6.Opacity(0.8f); menu.BackgroundColour = colourProvider.Background5; + menu.HoverColour = colourProvider.Background4; + menu.SelectionColour = colourProvider.Background3; Padding = new MarginPadding { Vertical = 20 }; } @@ -205,7 +205,8 @@ namespace osu.Game.Overlays.Rankings private void load(OverlayColourProvider colourProvider) { BackgroundColour = colourProvider.Background6.Opacity(0.5f); - BackgroundColourHover = colourProvider.Background5; + // osu-web adds a 0.6 opacity container on top of the 0.5 base one when hovering, 0.8 on a single container here matches the resulting colour + BackgroundColourHover = colourProvider.Background6.Opacity(0.8f); } } } From 61127a389c19e348e3d341e98abd945cdff59538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 Oct 2021 13:31:49 +0200 Subject: [PATCH 53/79] Fix tab dropdown receiving accent colour too early --- .../Graphics/UserInterface/OsuTabDropdown.cs | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs index 7f21f69065..fd1bbcd1e3 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs @@ -22,22 +22,34 @@ namespace osu.Game.Graphics.UserInterface { accentColour = value; - if (Menu is OsuDropdownMenu dropdownMenu) - { - dropdownMenu.HoverColour = value; - dropdownMenu.SelectionColour = value.Opacity(0.5f); - } - - if (Header is OsuTabDropdownHeader tabDropdownHeader) - tabDropdownHeader.AccentColour = value; + if (IsLoaded) + propagateAccentColour(); } } + private void propagateAccentColour() + { + if (Menu is OsuDropdownMenu dropdownMenu) + { + dropdownMenu.HoverColour = accentColour; + dropdownMenu.SelectionColour = accentColour.Opacity(0.5f); + } + + if (Header is OsuTabDropdownHeader tabDropdownHeader) + tabDropdownHeader.AccentColour = accentColour; + } + public OsuTabDropdown() { RelativeSizeAxes = Axes.X; } + protected override void LoadComplete() + { + base.LoadComplete(); + propagateAccentColour(); + } + protected override DropdownMenu CreateMenu() => new OsuTabDropdownMenu(); protected override DropdownHeader CreateHeader() => new OsuTabDropdownHeader @@ -80,7 +92,7 @@ namespace osu.Game.Graphics.UserInterface { accentColour = value; BackgroundColourHover = value; - Foreground.Colour = value; + updateColour(); } } @@ -116,15 +128,20 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { - Foreground.Colour = BackgroundColour; + updateColour(); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - Foreground.Colour = BackgroundColourHover; + updateColour(); base.OnHoverLost(e); } + + private void updateColour() + { + Foreground.Colour = IsHovered ? BackgroundColour : BackgroundColourHover; + } } } } From 80da15369757716ce3ed7c7c82a96b4491447e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Oct 2021 22:47:18 +0200 Subject: [PATCH 54/79] Recolour a few other existing dropdowns with same hover & selection colours --- osu.Game/Overlays/Login/UserDropdown.cs | 3 ++- osu.Game/Overlays/Music/CollectionDropdown.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Login/UserDropdown.cs b/osu.Game/Overlays/Login/UserDropdown.cs index d7c47351ec..5c3a41aec9 100644 --- a/osu.Game/Overlays/Login/UserDropdown.cs +++ b/osu.Game/Overlays/Login/UserDropdown.cs @@ -50,7 +50,8 @@ namespace osu.Game.Overlays.Login private void load(OsuColour colours) { BackgroundColour = colours.Gray3; - HoverColour = SelectionColour = colours.Gray5; + SelectionColour = colours.Gray4; + HoverColour = colours.Gray5; } protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableUserDropdownMenuItem(item); diff --git a/osu.Game/Overlays/Music/CollectionDropdown.cs b/osu.Game/Overlays/Music/CollectionDropdown.cs index d4686dee10..658eebe67b 100644 --- a/osu.Game/Overlays/Music/CollectionDropdown.cs +++ b/osu.Game/Overlays/Music/CollectionDropdown.cs @@ -35,7 +35,8 @@ namespace osu.Game.Overlays.Music private void load(OsuColour colours) { BackgroundColour = colours.Gray4; - HoverColour = SelectionColour = colours.Gray6; + SelectionColour = colours.Gray5; + HoverColour = colours.Gray6; } } From f29eb08d93cc2982585752f7522dbab126778e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Oct 2021 22:55:49 +0200 Subject: [PATCH 55/79] Improve contrast in default `OverlayColourProvider`-themed appearance --- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 021f36d4fd..b1d4691938 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -54,8 +54,8 @@ namespace osu.Game.Graphics.UserInterface private void load(OverlayColourProvider? colourProvider, OsuColour colours, AudioManager audio) { BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); - HoverColour = colourProvider?.Highlight1 ?? colours.PinkDarker; - SelectionColour = colourProvider?.Light4 ?? colours.PinkDarker.Opacity(0.5f); + HoverColour = colourProvider?.Light4 ?? colours.PinkDarker; + SelectionColour = colourProvider?.Background3 ?? colours.PinkDarker.Opacity(0.5f); sampleOpen = audio.Samples.Get(@"UI/dropdown-open"); sampleClose = audio.Samples.Get(@"UI/dropdown-close"); @@ -344,7 +344,7 @@ namespace osu.Game.Graphics.UserInterface private void load(OverlayColourProvider? colourProvider, OsuColour colours) { BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); - BackgroundColourHover = colourProvider?.Highlight1 ?? colours.PinkDarker; + BackgroundColourHover = colourProvider?.Light4 ?? colours.PinkDarker; } } } From 37841ca3aa001a1df9eeecc6447865d4fe78d16a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Oct 2021 10:41:34 +0900 Subject: [PATCH 56/79] Remove incorrect `ToString` calls causing localisation to not actually apply --- osu.Game/Overlays/OSD/TrackedSettingToast.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs index 198aa1438a..51214fe460 100644 --- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs +++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.OSD private Sample sampleChange; public TrackedSettingToast(SettingDescription description) - : base(description.Name.ToString(), description.Value.ToString(), description.Shortcut.ToString()) + : base(description.Name, description.Value, description.Shortcut) { FillFlowContainer optionLights; From 4b2eb7736c7aecebeab7f28c2bdc76d9228e384d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Oct 2021 12:52:38 +0900 Subject: [PATCH 57/79] Move helper method to bottom of file --- .../Graphics/UserInterface/OsuTabDropdown.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs index fd1bbcd1e3..68ffc6bf4e 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabDropdown.cs @@ -27,18 +27,6 @@ namespace osu.Game.Graphics.UserInterface } } - private void propagateAccentColour() - { - if (Menu is OsuDropdownMenu dropdownMenu) - { - dropdownMenu.HoverColour = accentColour; - dropdownMenu.SelectionColour = accentColour.Opacity(0.5f); - } - - if (Header is OsuTabDropdownHeader tabDropdownHeader) - tabDropdownHeader.AccentColour = accentColour; - } - public OsuTabDropdown() { RelativeSizeAxes = Axes.X; @@ -58,6 +46,18 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.TopRight }; + private void propagateAccentColour() + { + if (Menu is OsuDropdownMenu dropdownMenu) + { + dropdownMenu.HoverColour = accentColour; + dropdownMenu.SelectionColour = accentColour.Opacity(0.5f); + } + + if (Header is OsuTabDropdownHeader tabDropdownHeader) + tabDropdownHeader.AccentColour = accentColour; + } + private class OsuTabDropdownMenu : OsuDropdownMenu { public OsuTabDropdownMenu() From a06f624a4a20f43aebfd50c46a6d705af121116d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 Oct 2021 15:03:15 +0900 Subject: [PATCH 58/79] Fix possible exception with "invalid" key Triggered by toggling Shift+Tab during gameplay. --- osu.Game/Localisation/ResourceManagerLocalisationStore.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Localisation/ResourceManagerLocalisationStore.cs b/osu.Game/Localisation/ResourceManagerLocalisationStore.cs index a35ce7a9c8..6a4e38fb38 100644 --- a/osu.Game/Localisation/ResourceManagerLocalisationStore.cs +++ b/osu.Game/Localisation/ResourceManagerLocalisationStore.cs @@ -29,6 +29,9 @@ namespace osu.Game.Localisation { var split = lookup.Split(':'); + if (split.Length < 2) + return null; + string ns = split[0]; string key = split[1]; From c8cdc38efd8419bf12c0a48ea8c41fbb93f7bfdd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 Oct 2021 15:20:30 +0900 Subject: [PATCH 59/79] Always compare OnlineIds by >0 --- osu.Game/Models/RealmBeatmapSet.cs | 2 +- osu.Game/Stores/BeatmapImporter.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Models/RealmBeatmapSet.cs b/osu.Game/Models/RealmBeatmapSet.cs index d6e56fd61c..6735510422 100644 --- a/osu.Game/Models/RealmBeatmapSet.cs +++ b/osu.Game/Models/RealmBeatmapSet.cs @@ -63,7 +63,7 @@ namespace osu.Game.Models if (IsManaged && other.IsManaged) return ID == other.ID; - if (OnlineID >= 0 && other.OnlineID >= 0) + if (OnlineID > 0 && other.OnlineID > 0) return OnlineID == other.OnlineID; if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash)) diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index dc43fda09a..254127cc7e 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -74,7 +74,7 @@ namespace osu.Game.Stores // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineID > 0)) { - if (beatmapSet.OnlineID > -1) + if (beatmapSet.OnlineID > 0) { beatmapSet.OnlineID = -1; LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); @@ -91,7 +91,7 @@ namespace osu.Game.Stores // If this is ever an issue, we can consider marking as pending delete but not resetting the IDs (but care will be required for // beatmaps, which don't have their own `DeletePending` state). - if (beatmapSet.OnlineID > -1) + if (beatmapSet.OnlineID > 0) { var existingSetWithSameOnlineID = realm.All().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID); @@ -110,7 +110,7 @@ namespace osu.Game.Stores private void validateOnlineIds(RealmBeatmapSet beatmapSet, Realm realm) { - var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineID > -1).Select(b => b.OnlineID).ToList(); + var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineID > 0).Select(b => b.OnlineID).ToList(); // ensure all IDs are unique if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1)) @@ -148,7 +148,7 @@ namespace osu.Game.Stores if (!base.CanSkipImport(existing, import)) return false; - return existing.Beatmaps.Any(b => b.OnlineID > -1); + return existing.Beatmaps.Any(b => b.OnlineID > 0); } protected override bool CanReuseExisting(RealmBeatmapSet existing, RealmBeatmapSet import) From da750a74fc91d26ba39205201a33ac1d6b5b49fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Oct 2021 15:24:27 +0900 Subject: [PATCH 60/79] Add xmldoc mention of valid `OnlineID` values --- osu.Game/Database/IHasOnlineID.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/IHasOnlineID.cs b/osu.Game/Database/IHasOnlineID.cs index 529c68a8f8..6e2be7e1f9 100644 --- a/osu.Game/Database/IHasOnlineID.cs +++ b/osu.Game/Database/IHasOnlineID.cs @@ -8,8 +8,12 @@ namespace osu.Game.Database public interface IHasOnlineID { /// - /// The server-side ID representing this instance, if one exists. -1 denotes a missing ID. + /// The server-side ID representing this instance, if one exists. Any value 0 or less denotes a missing ID. /// + /// + /// Generally we use -1 when specifying "missing" in code, but values of 0 are also considered missing as the online source + /// is generally a MySQL autoincrement value, which can never be 0. + /// int OnlineID { get; } } } From 6f38e6166d136bbe46ce8501e6f796402ae33911 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 20 Oct 2021 10:32:00 +0300 Subject: [PATCH 61/79] Fix and improve TestSceneUserProfilePreviousUsernames --- .../TestSceneUserProfilePreviousUsernames.cs | 81 +++++++++++++------ 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs index 1e9d62f379..83254b616c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs @@ -20,44 +20,75 @@ namespace osu.Game.Tests.Visual.Online private IAPIProvider api { get; set; } private readonly Bindable user = new Bindable(); + private GetUserRequest request; + private PreviousUsernames container; - public TestSceneUserProfilePreviousUsernames() + [SetUp] + public void SetUp() { - Child = new PreviousUsernames + request?.Cancel(); + + Child = container = new PreviousUsernames { Anchor = Anchor.Centre, Origin = Anchor.Centre, User = { BindTarget = user }, }; - - User[] users = - { - new User { PreviousUsernames = new[] { "username1" } }, - new User { PreviousUsernames = new[] { "longusername", "longerusername" } }, - new User { PreviousUsernames = new[] { "test", "angelsim", "verylongusername" } }, - new User { PreviousUsernames = new[] { "ihavenoidea", "howcani", "makethistext", "anylonger" } }, - new User { PreviousUsernames = Array.Empty() }, - null - }; - - AddStep("single username", () => user.Value = users[0]); - AddStep("two usernames", () => user.Value = users[1]); - AddStep("three usernames", () => user.Value = users[2]); - AddStep("four usernames", () => user.Value = users[3]); - AddStep("no username", () => user.Value = users[4]); - AddStep("null user", () => user.Value = users[5]); } - protected override void LoadComplete() + [Test] + public void TestOffline() { - base.LoadComplete(); + AddAssert("Is Hidden", () => container?.Alpha == 0); - AddStep("online user (Angelsim)", () => + AddStep("1 username", () => user.Value = users[0]); + AddUntilStep("Is visible", () => container?.Alpha == 1); + + AddStep("2 usernames", () => user.Value = users[1]); + AddUntilStep("Is visible", () => container?.Alpha == 1); + + AddStep("3 usernames", () => user.Value = users[2]); + AddUntilStep("Is visible", () => container?.Alpha == 1); + + AddStep("4 usernames", () => user.Value = users[3]); + AddUntilStep("Is visible", () => container?.Alpha == 1); + + AddStep("No username", () => user.Value = users[4]); + AddUntilStep("Is hidden", () => container?.Alpha == 0); + + AddStep("Null user", () => user.Value = users[5]); + AddUntilStep("Is hidden", () => container?.Alpha == 0); + } + + [Test] + public void TestOnline() + { + AddAssert("Is Hidden", () => container?.Alpha == 0); + + AddStep("Create request", () => { - var request = new GetUserRequest(1777162); - request.Success += user => this.user.Value = user; - api.Queue(request); + request = new GetUserRequest(1777162); + request.Success += u => user.Value = u; + api?.Queue(request); }); + + AddUntilStep("Is visible", () => container?.Alpha == 1); + } + + private static readonly User[] users = + { + new User { Id = 1, PreviousUsernames = new[] { "username1" } }, + new User { Id = 2, PreviousUsernames = new[] { "longusername", "longerusername" } }, + new User { Id = 3, PreviousUsernames = new[] { "test", "angelsim", "verylongusername" } }, + new User { Id = 4, PreviousUsernames = new[] { "ihavenoidea", "howcani", "makethistext", "anylonger" } }, + new User { Id = 5, PreviousUsernames = Array.Empty() }, + null + }; + + protected override void Dispose(bool isDisposing) + { + request?.Cancel(); + base.Dispose(isDisposing); } } } From faabc75a3ec41f01a5a9030fbf4c6ef989d8797b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 20 Oct 2021 10:54:08 +0300 Subject: [PATCH 62/79] Fix failing test --- .../TestSceneUserProfilePreviousUsernames.cs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs index 83254b616c..303f66d893 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs @@ -4,7 +4,6 @@ using System; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -19,12 +18,11 @@ namespace osu.Game.Tests.Visual.Online [Resolved] private IAPIProvider api { get; set; } - private readonly Bindable user = new Bindable(); private GetUserRequest request; private PreviousUsernames container; [SetUp] - public void SetUp() + public void SetUp() => Schedule(() => { request?.Cancel(); @@ -32,31 +30,30 @@ namespace osu.Game.Tests.Visual.Online { Anchor = Anchor.Centre, Origin = Anchor.Centre, - User = { BindTarget = user }, }; - } + }); [Test] public void TestOffline() { AddAssert("Is Hidden", () => container?.Alpha == 0); - AddStep("1 username", () => user.Value = users[0]); + AddStep("1 username", () => container.User.Value = users[0]); AddUntilStep("Is visible", () => container?.Alpha == 1); - AddStep("2 usernames", () => user.Value = users[1]); + AddStep("2 usernames", () => container.User.Value = users[1]); AddUntilStep("Is visible", () => container?.Alpha == 1); - AddStep("3 usernames", () => user.Value = users[2]); + AddStep("3 usernames", () => container.User.Value = users[2]); AddUntilStep("Is visible", () => container?.Alpha == 1); - AddStep("4 usernames", () => user.Value = users[3]); + AddStep("4 usernames", () => container.User.Value = users[3]); AddUntilStep("Is visible", () => container?.Alpha == 1); - AddStep("No username", () => user.Value = users[4]); + AddStep("No username", () => container.User.Value = users[4]); AddUntilStep("Is hidden", () => container?.Alpha == 0); - AddStep("Null user", () => user.Value = users[5]); + AddStep("Null user", () => container.User.Value = users[5]); AddUntilStep("Is hidden", () => container?.Alpha == 0); } @@ -68,7 +65,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("Create request", () => { request = new GetUserRequest(1777162); - request.Success += u => user.Value = u; + request.Success += u => container.User.Value = u; api?.Queue(request); }); From 0f8d270442b34d4155f13991e0e42adc5e41edfe Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 20 Oct 2021 11:27:24 +0300 Subject: [PATCH 63/79] Remove online part since it doesn't really check anything --- .../TestSceneUserProfilePreviousUsernames.cs | 46 ++++--------------- 1 file changed, 8 insertions(+), 38 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs index 303f66d893..b5d2d15392 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs @@ -3,10 +3,7 @@ using System; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Users; @@ -15,17 +12,11 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneUserProfilePreviousUsernames : OsuTestScene { - [Resolved] - private IAPIProvider api { get; set; } - - private GetUserRequest request; private PreviousUsernames container; [SetUp] public void SetUp() => Schedule(() => { - request?.Cancel(); - Child = container = new PreviousUsernames { Anchor = Anchor.Centre, @@ -34,42 +25,27 @@ namespace osu.Game.Tests.Visual.Online }); [Test] - public void TestOffline() + public void TestVisibility() { - AddAssert("Is Hidden", () => container?.Alpha == 0); + AddAssert("Is Hidden", () => container.Alpha == 0); AddStep("1 username", () => container.User.Value = users[0]); - AddUntilStep("Is visible", () => container?.Alpha == 1); + AddUntilStep("Is visible", () => container.Alpha == 1); AddStep("2 usernames", () => container.User.Value = users[1]); - AddUntilStep("Is visible", () => container?.Alpha == 1); + AddUntilStep("Is visible", () => container.Alpha == 1); AddStep("3 usernames", () => container.User.Value = users[2]); - AddUntilStep("Is visible", () => container?.Alpha == 1); + AddUntilStep("Is visible", () => container.Alpha == 1); AddStep("4 usernames", () => container.User.Value = users[3]); - AddUntilStep("Is visible", () => container?.Alpha == 1); + AddUntilStep("Is visible", () => container.Alpha == 1); AddStep("No username", () => container.User.Value = users[4]); - AddUntilStep("Is hidden", () => container?.Alpha == 0); + AddUntilStep("Is hidden", () => container.Alpha == 0); AddStep("Null user", () => container.User.Value = users[5]); - AddUntilStep("Is hidden", () => container?.Alpha == 0); - } - - [Test] - public void TestOnline() - { - AddAssert("Is Hidden", () => container?.Alpha == 0); - - AddStep("Create request", () => - { - request = new GetUserRequest(1777162); - request.Success += u => container.User.Value = u; - api?.Queue(request); - }); - - AddUntilStep("Is visible", () => container?.Alpha == 1); + AddUntilStep("Is hidden", () => container.Alpha == 0); } private static readonly User[] users = @@ -81,11 +57,5 @@ namespace osu.Game.Tests.Visual.Online new User { Id = 5, PreviousUsernames = Array.Empty() }, null }; - - protected override void Dispose(bool isDisposing) - { - request?.Cancel(); - base.Dispose(isDisposing); - } } } From a7f3beabe3561d371e248025f62532f0541e7cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 Oct 2021 12:36:21 +0200 Subject: [PATCH 64/79] Modify `OsuTextBox` test scene to test against colour provider --- .../UserInterface/TestSceneOsuTextBox.cs | 93 ++++++++----------- .../UserInterface/ThemeComparisonTestScene.cs | 4 +- 2 files changed, 42 insertions(+), 55 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs index 756928d3ec..fc1866cdf3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs @@ -1,80 +1,67 @@ // 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 NUnit.Framework; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osuTK; -using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneOsuTextBox : OsuTestScene + public class TestSceneOsuTextBox : ThemeComparisonTestScene { - private readonly OsuNumberBox numberBox; + private IEnumerable numberBoxes => this.ChildrenOfType(); - public TestSceneOsuTextBox() + protected override Drawable CreateContent() => new FillFlowContainer { - Child = new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(50f), + Spacing = new Vector2(0f, 50f), + Children = new[] { - Masking = true, - CornerRadius = 10f, - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding(15f), - Children = new Drawable[] + new OsuTextBox { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.DarkSlateGray, - Alpha = 0.75f, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Padding = new MarginPadding(50f), - Spacing = new Vector2(0f, 50f), - Children = new[] - { - new OsuTextBox - { - Width = 500f, - PlaceholderText = "Normal textbox", - }, - new OsuPasswordTextBox - { - Width = 500f, - PlaceholderText = "Password textbox", - }, - numberBox = new OsuNumberBox - { - Width = 500f, - PlaceholderText = "Number textbox" - } - } - } + RelativeSizeAxes = Axes.X, + PlaceholderText = "Normal textbox", + }, + new OsuPasswordTextBox + { + RelativeSizeAxes = Axes.X, + PlaceholderText = "Password textbox", + }, + new OsuNumberBox + { + RelativeSizeAxes = Axes.X, + PlaceholderText = "Number textbox" } - }; - } + } + }; [Test] public void TestNumberBox() { - clearTextbox(numberBox); - AddStep("enter numbers", () => numberBox.Text = "987654321"); - expectedValue(numberBox, "987654321"); + AddStep("create themed content", () => CreateThemedContent(OverlayColourScheme.Red)); - clearTextbox(numberBox); - AddStep("enter text + single number", () => numberBox.Text = "1 hello 2 world 3"); - expectedValue(numberBox, "123"); + clearTextboxes(numberBoxes); + AddStep("enter numbers", () => numberBoxes.ForEach(numberBox => numberBox.Text = "987654321")); + expectedValue(numberBoxes, "987654321"); - clearTextbox(numberBox); + clearTextboxes(numberBoxes); + AddStep("enter text + single number", () => numberBoxes.ForEach(numberBox => numberBox.Text = "1 hello 2 world 3")); + expectedValue(numberBoxes, "123"); + + clearTextboxes(numberBoxes); } - private void clearTextbox(OsuTextBox textBox) => AddStep("clear textbox", () => textBox.Text = null); - private void expectedValue(OsuTextBox textBox, string value) => AddAssert("expected textbox value", () => textBox.Text == value); + private void clearTextboxes(IEnumerable textBoxes) => AddStep("clear textbox", () => textBoxes.ForEach(textBox => textBox.Text = null)); + private void expectedValue(IEnumerable textBoxes, string value) => AddAssert("expected textbox value", () => textBoxes.All(textbox => textbox.Text == value)); } } diff --git a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs index f8b9e8223b..db1c90f287 100644 --- a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs +++ b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface }); } - private void createThemedContent(OverlayColourScheme colourScheme) + protected void CreateThemedContent(OverlayColourScheme colourScheme) { var colourProvider = new OverlayColourProvider(colourScheme); @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestAllColourSchemes() { foreach (var scheme in Enum.GetValues(typeof(OverlayColourScheme)).Cast()) - AddStep($"set {scheme} scheme", () => createThemedContent(scheme)); + AddStep($"set {scheme} scheme", () => CreateThemedContent(scheme)); } } } From addcef4f5d9e6abca6776149a391401a350f71e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 Oct 2021 12:51:51 +0200 Subject: [PATCH 65/79] Recolour text box using `OverlayColourProvider` --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 75af9efc38..b8f8518bb4 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -17,18 +19,13 @@ using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Graphics.UserInterface { public class OsuTextBox : BasicTextBox { - private readonly Sample[] textAddedSamples = new Sample[4]; - private Sample capsTextAddedSample; - private Sample textRemovedSample; - private Sample textCommittedSample; - private Sample caretMovedSample; - /// /// Whether to allow playing a different samples based on the type of character. /// If set to false, the same sample will be used for all characters. @@ -42,10 +39,15 @@ namespace osu.Game.Graphics.UserInterface protected override SpriteText CreatePlaceholder() => new OsuSpriteText { Font = OsuFont.GetFont(italics: true), - Colour = new Color4(180, 180, 180, 255), Margin = new MarginPadding { Left = 2 }, }; + private readonly Sample?[] textAddedSamples = new Sample[4]; + private Sample? capsTextAddedSample; + private Sample? textRemovedSample; + private Sample? textCommittedSample; + private Sample? caretMovedSample; + public OsuTextBox() { Height = 40; @@ -56,12 +58,14 @@ namespace osu.Game.Graphics.UserInterface Current.DisabledChanged += disabled => { Alpha = disabled ? 0.3f : 1; }; } - [BackgroundDependencyLoader] - private void load(OsuColour colour, AudioManager audio) + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider, OsuColour colour, AudioManager audio) { - BackgroundUnfocused = Color4.Black.Opacity(0.5f); - BackgroundFocused = OsuColour.Gray(0.3f).Opacity(0.8f); - BackgroundCommit = BorderColour = colour.Yellow; + BackgroundUnfocused = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); + BackgroundFocused = colourProvider?.Background3 ?? OsuColour.Gray(0.3f).Opacity(0.8f); + BackgroundCommit = BorderColour = colourProvider?.Highlight1 ?? colour.Yellow; + + Placeholder.Colour = colourProvider?.Foreground1 ?? new Color4(180, 180, 180, 255); for (int i = 0; i < textAddedSamples.Length; i++) textAddedSamples[i] = audio.Samples.Get($@"Keyboard/key-press-{1 + i}"); From 1ec881ce1d9306c4d3b326b1d44e81a64206a490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 Oct 2021 12:58:37 +0200 Subject: [PATCH 66/79] Recolour focused text box variant --- osu.Game/Graphics/UserInterface/FocusedTextBox.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index ceea9620c8..88608bf43c 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Input.Events; @@ -8,6 +10,7 @@ using osu.Framework.Platform; using osu.Game.Input.Bindings; using osuTK.Input; using osu.Framework.Input.Bindings; +using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterface { @@ -42,13 +45,13 @@ namespace osu.Game.Graphics.UserInterface } [Resolved] - private GameHost host { get; set; } + private GameHost? host { get; set; } - [BackgroundDependencyLoader] - private void load() + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider) { - BackgroundUnfocused = new Color4(10, 10, 10, 255); - BackgroundFocused = new Color4(10, 10, 10, 255); + BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); + BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); } // We may not be focused yet, but we need to handle keyboard input to be able to request focus From 9ad9465020eee89a421f80d9a5ce2027548a65ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 Oct 2021 13:10:23 +0200 Subject: [PATCH 67/79] Remove online-screen-local textbox recolours --- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 13 +--------- .../Match/Components/RoomSettingsOverlay.cs | 26 ------------------- .../Match/MultiplayerMatchSettingsOverlay.cs | 6 ++--- .../Playlists/PlaylistsRoomSettingsOverlay.cs | 8 +++--- 4 files changed, 8 insertions(+), 45 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 08bdd0487a..62012906a7 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -17,7 +17,6 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; @@ -126,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { RelativeSizeAxes = Axes.X, Height = Header.HEIGHT, - Child = searchTextBox = new LoungeSearchTextBox + Child = searchTextBox = new SearchTextBox { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, @@ -362,15 +361,5 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected abstract RoomSubScreen CreateRoomSubScreen(Room room); protected abstract ListingPollingComponent CreatePollingComponent(); - - private class LoungeSearchTextBox : SearchTextBox - { - [BackgroundDependencyLoader] - private void load() - { - BackgroundUnfocused = OsuColour.Gray(0.06f); - BackgroundFocused = OsuColour.Gray(0.12f); - } - } } } diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs index a6cdde14f6..6d14b95aec 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs @@ -12,7 +12,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Online.Rooms; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Match.Components { @@ -91,31 +90,6 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components { } - protected class SettingsTextBox : OsuTextBox - { - [BackgroundDependencyLoader] - private void load() - { - BackgroundUnfocused = Color4.Black; - BackgroundFocused = Color4.Black; - } - } - - protected class SettingsNumberTextBox : SettingsTextBox - { - protected override bool CanAddCharacter(char character) => char.IsNumber(character); - } - - protected class SettingsPasswordTextBox : OsuPasswordTextBox - { - [BackgroundDependencyLoader] - private void load() - { - BackgroundUnfocused = Color4.Black; - BackgroundFocused = Color4.Black; - } - } - protected class SectionContainer : FillFlowContainer
{ public SectionContainer() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 0edf5dde6d..5bc76a10bc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -153,7 +153,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { new Section("Room name") { - Child = NameField = new SettingsTextBox + Child = NameField = new OsuTextBox { RelativeSizeAxes = Axes.X, TabbableContentContainer = this, @@ -202,7 +202,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match new Section("Max participants") { Alpha = disabled_alpha, - Child = MaxParticipantsField = new SettingsNumberTextBox + Child = MaxParticipantsField = new OsuNumberBox { RelativeSizeAxes = Axes.X, TabbableContentContainer = this, @@ -211,7 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }, new Section("Password (optional)") { - Child = PasswordTextBox = new SettingsPasswordTextBox + Child = PasswordTextBox = new OsuPasswordTextBox { RelativeSizeAxes = Axes.X, TabbableContentContainer = this, diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 9e000aa712..c2bd7730e9 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -121,7 +121,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { new Section("Room name") { - Child = NameField = new SettingsTextBox + Child = NameField = new OsuTextBox { RelativeSizeAxes = Axes.X, TabbableContentContainer = this, @@ -150,7 +150,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }, new Section("Allowed attempts (across all playlist items)") { - Child = MaxAttemptsField = new SettingsNumberTextBox + Child = MaxAttemptsField = new OsuNumberBox { RelativeSizeAxes = Axes.X, TabbableContentContainer = this, @@ -168,7 +168,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new Section("Max participants") { Alpha = disabled_alpha, - Child = MaxParticipantsField = new SettingsNumberTextBox + Child = MaxParticipantsField = new OsuNumberBox { RelativeSizeAxes = Axes.X, TabbableContentContainer = this, @@ -178,7 +178,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new Section("Password (optional)") { Alpha = disabled_alpha, - Child = new SettingsPasswordTextBox + Child = new OsuPasswordTextBox { RelativeSizeAxes = Axes.X, TabbableContentContainer = this, From 7bc8f5cd5cf4628626c76e65c81484b3609556a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Oct 2021 12:59:40 +0900 Subject: [PATCH 68/79] Change selection colour to also match the colour provider scheme --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index b8f8518bb4..96319b9fdd 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -48,6 +48,8 @@ namespace osu.Game.Graphics.UserInterface private Sample? textCommittedSample; private Sample? caretMovedSample; + private OsuCaret? caret; + public OsuTextBox() { Height = 40; @@ -62,8 +64,12 @@ namespace osu.Game.Graphics.UserInterface private void load(OverlayColourProvider? colourProvider, OsuColour colour, AudioManager audio) { BackgroundUnfocused = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); - BackgroundFocused = colourProvider?.Background3 ?? OsuColour.Gray(0.3f).Opacity(0.8f); + BackgroundFocused = colourProvider?.Background4 ?? OsuColour.Gray(0.3f).Opacity(0.8f); BackgroundCommit = BorderColour = colourProvider?.Highlight1 ?? colour.Yellow; + selectionColour = colourProvider?.Background1 ?? new Color4(249, 90, 255, 255); + + if (caret != null) + caret.SelectionColour = selectionColour; Placeholder.Colour = colourProvider?.Foreground1 ?? new Color4(180, 180, 180, 255); @@ -76,7 +82,9 @@ namespace osu.Game.Graphics.UserInterface caretMovedSample = audio.Samples.Get(@"Keyboard/key-movement"); } - protected override Color4 SelectionColour => new Color4(249, 90, 255, 255); + private Color4 selectionColour; + + protected override Color4 SelectionColour => selectionColour; protected override void OnUserTextAdded(string added) { @@ -128,7 +136,7 @@ namespace osu.Game.Graphics.UserInterface Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) }, }; - protected override Caret CreateCaret() => new OsuCaret + protected override Caret CreateCaret() => caret = new OsuCaret { CaretWidth = CaretWidth, SelectionColour = SelectionColour, From a5c155bc87354095b864bb5296b451b6e2ce8faa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Oct 2021 16:43:46 +0900 Subject: [PATCH 69/79] Remove `APIPlaylistBeatmap` subclass --- .../API/Requests/Responses/APIBeatmap.cs | 4 ++++ osu.Game/Online/Rooms/APIPlaylistBeatmap.cs | 23 ------------------- osu.Game/Online/Rooms/PlaylistItem.cs | 3 ++- 3 files changed, 6 insertions(+), 24 deletions(-) delete mode 100644 osu.Game/Online/Rooms/APIPlaylistBeatmap.cs diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index c2a68c8ca1..ea4265e641 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -19,6 +19,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"status")] public BeatmapSetOnlineStatus Status { get; set; } + [JsonProperty("checksum")] + public string Checksum { get; set; } + [JsonProperty(@"beatmapset")] public APIBeatmapSet BeatmapSet { get; set; } @@ -78,6 +81,7 @@ namespace osu.Game.Online.API.Requests.Responses // this is actually an incorrect mapping (Length is calculated as drain length in lazer's import process, see BeatmapManager.calculateLength). Length = TimeSpan.FromSeconds(length).TotalMilliseconds, Status = Status, + MD5Hash = Checksum, BeatmapSet = set, Metrics = metrics, MaxCombo = maxCombo, diff --git a/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs b/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs deleted file mode 100644 index 00623282d3..0000000000 --- a/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using Newtonsoft.Json; -using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets; - -namespace osu.Game.Online.Rooms -{ - public class APIPlaylistBeatmap : APIBeatmap - { - [JsonProperty("checksum")] - public string Checksum { get; set; } - - public override BeatmapInfo ToBeatmapInfo(RulesetStore rulesets) - { - var b = base.ToBeatmapInfo(rulesets); - b.MD5Hash = Checksum; - return b; - } - } -} diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 48f1347fa1..7fcce1514d 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -42,7 +43,7 @@ namespace osu.Game.Online.Rooms public readonly BindableList RequiredMods = new BindableList(); [JsonProperty("beatmap")] - private APIPlaylistBeatmap apiBeatmap { get; set; } + private APIBeatmap apiBeatmap { get; set; } private APIMod[] allowedModsBacking; From 0706ad70fb6f36ba9dcad45b2c9da451c6f22a6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 Oct 2021 18:43:48 +0900 Subject: [PATCH 70/79] Move `BeatmapSetOnlineInfo` to an interface type --- .../Online/TestSceneBeatmapAvailability.cs | 9 +- .../Online/TestSceneBeatmapSetOverlay.cs | 9 +- .../TestSceneBeatmapSetOverlayDetails.cs | 3 +- .../Online/TestSceneDirectDownloadButton.cs | 3 +- .../Visual/Online/TestSceneDirectPanel.cs | 5 +- .../TestSceneBeatmapListingSearchControl.cs | 5 +- .../TestSceneDashboardBeatmapListing.cs | 9 +- .../TestSceneUpdateableBeatmapSetCover.cs | 7 +- .../Components/TournamentBeatmapPanel.cs | 19 ++- .../Screens/MapPool/MapPoolScreen.cs | 6 +- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 11 +- osu.Game/Beatmaps/BeatmapSetInfo.cs | 144 +++++++++++++++++- osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs | 2 +- .../Beatmaps/Drawables/BeatmapSetCover.cs | 10 +- .../Drawables/UpdateableBeatmapSetCover.cs | 6 +- .../API/Requests/Responses/APIBeatmapSet.cs | 55 +++---- .../OnlinePlayBeatmapAvailabilityTracker.cs | 6 +- .../OnlinePlay/Components/BeatmapTitle.cs | 2 +- osu.Game/Tests/Beatmaps/TestBeatmap.cs | 3 +- 19 files changed, 222 insertions(+), 92 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs index fe94165777..6f9744ca73 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.BeatmapSet; namespace osu.Game.Tests.Visual.Online @@ -22,7 +23,7 @@ namespace osu.Game.Tests.Visual.Online { AddStep("set undownloadable beatmapset with link", () => container.BeatmapSet = new BeatmapSetInfo { - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Availability = new BeatmapSetOnlineAvailability { @@ -40,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online { AddStep("set undownloadable beatmapset without link", () => container.BeatmapSet = new BeatmapSetInfo { - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Availability = new BeatmapSetOnlineAvailability { @@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Online { AddStep("set parts-removed beatmapset with link", () => container.BeatmapSet = new BeatmapSetInfo { - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Availability = new BeatmapSetOnlineAvailability { @@ -75,7 +76,7 @@ namespace osu.Game.Tests.Visual.Online { AddStep("set normal beatmapset", () => container.BeatmapSet = new BeatmapSetInfo { - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Availability = new BeatmapSetOnlineAvailability { diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index 453e26ef96..ef89a86e79 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -11,6 +11,7 @@ using osu.Game.Users; using System; using System.Collections.Generic; using System.Linq; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Tests.Visual.Online { @@ -63,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online Id = 3, }, }, - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Preview = @"https://b.ppy.sh/preview/12345.mp3", PlayCount = 123, @@ -134,7 +135,7 @@ namespace osu.Game.Tests.Visual.Online Id = 3, }, }, - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Availability = new BeatmapSetOnlineAvailability { @@ -224,7 +225,7 @@ namespace osu.Game.Tests.Visual.Online Id = 3, } }, - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Covers = new BeatmapSetOnlineCovers(), }, @@ -309,7 +310,7 @@ namespace osu.Game.Tests.Visual.Online Id = 3, }, }, - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Preview = @"https://b.ppy.sh/preview/123.mp3", HasVideo = true, diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs index f7099b0615..c15c9f44e4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; using osu.Game.Screens.Select.Details; @@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Online }, } }, - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Status = BeatmapSetOnlineStatus.Ranked } diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs index 3fc894da0d..bb7fcc2fce 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Online; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Rulesets.Osu; using osu.Game.Tests.Resources; @@ -74,7 +75,7 @@ namespace osu.Game.Tests.Visual.Online { ID = 1, OnlineBeatmapSetID = 241526, - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Availability = new BeatmapSetOnlineAvailability { diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs index 722010ace2..6caca2a67c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Rulesets; using osu.Game.Users; @@ -31,7 +32,7 @@ namespace osu.Game.Tests.Visual.Online Id = 3, }, }, - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Availability = new BeatmapSetOnlineAvailability { @@ -86,7 +87,7 @@ namespace osu.Game.Tests.Visual.Online Id = 3, } }, - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { HasVideo = true, HasStoryboard = true, diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs index 008d91f649..a9fe7ed7d8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osuTK; @@ -111,7 +112,7 @@ namespace osu.Game.Tests.Visual.UserInterface private static readonly BeatmapSetInfo beatmap_set = new BeatmapSetInfo { - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Covers = new BeatmapSetOnlineCovers { @@ -122,7 +123,7 @@ namespace osu.Game.Tests.Visual.UserInterface private static readonly BeatmapSetInfo no_cover_beatmap_set = new BeatmapSetInfo { - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Covers = new BeatmapSetOnlineCovers { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs index c51204eaba..6727c7560b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs @@ -11,6 +11,7 @@ using osu.Game.Users; using System; using osu.Framework.Graphics.Shapes; using System.Collections.Generic; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Tests.Visual.UserInterface { @@ -69,7 +70,7 @@ namespace osu.Game.Tests.Visual.UserInterface Id = 100 } }, - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Covers = new BeatmapSetOnlineCovers { @@ -90,7 +91,7 @@ namespace osu.Game.Tests.Visual.UserInterface Id = 100 } }, - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Covers = new BeatmapSetOnlineCovers { @@ -115,7 +116,7 @@ namespace osu.Game.Tests.Visual.UserInterface Id = 100 } }, - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Covers = new BeatmapSetOnlineCovers { @@ -136,7 +137,7 @@ namespace osu.Game.Tests.Visual.UserInterface Id = 100 } }, - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Covers = new BeatmapSetOnlineCovers { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs index 4fef93e291..01e628075c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs @@ -13,6 +13,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; using osuTK; namespace osu.Game.Tests.Visual.UserInterface @@ -133,7 +134,7 @@ namespace osu.Game.Tests.Visual.UserInterface private static BeatmapSetInfo createBeatmapWithCover(string coverUrl) => new BeatmapSetInfo { - OnlineInfo = new BeatmapSetOnlineInfo + OnlineInfo = new APIBeatmapSet { Covers = new BeatmapSetOnlineCovers { Cover = coverUrl } } @@ -148,7 +149,7 @@ namespace osu.Game.Tests.Visual.UserInterface this.loadDelay = loadDelay; } - protected override Drawable CreateDrawable(BeatmapSetInfo model) + protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model) { if (model == null) return null; @@ -167,7 +168,7 @@ namespace osu.Game.Tests.Visual.UserInterface { private readonly int loadDelay; - public TestBeatmapSetCover(BeatmapSetInfo set, int loadDelay) + public TestBeatmapSetCover(IBeatmapSetOnlineInfo set, int loadDelay) : base(set) { this.loadDelay = loadDelay; diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 0e5a66e7fe..a31430495d 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; -using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -21,7 +20,8 @@ namespace osu.Game.Tournament.Components { public class TournamentBeatmapPanel : CompositeDrawable { - public readonly BeatmapInfo BeatmapInfo; + public readonly IBeatmapInfo BeatmapInfo; + private readonly string mod; private const float horizontal_padding = 10; @@ -32,12 +32,13 @@ namespace osu.Game.Tournament.Components private readonly Bindable currentMatch = new Bindable(); private Box flash; - public TournamentBeatmapPanel(BeatmapInfo beatmapInfo, string mod = null) + public TournamentBeatmapPanel(IBeatmapInfo beatmapInfo, string mod = null) { if (beatmapInfo == null) throw new ArgumentNullException(nameof(beatmapInfo)); BeatmapInfo = beatmapInfo; this.mod = mod; + Width = 400; Height = HEIGHT; } @@ -61,7 +62,7 @@ namespace osu.Game.Tournament.Components { RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(0.5f), - BeatmapSet = BeatmapInfo.BeatmapSet, + BeatmapSet = BeatmapInfo.BeatmapSet as IBeatmapSetOnlineInfo, }, new FillFlowContainer { @@ -74,9 +75,7 @@ namespace osu.Game.Tournament.Components { new TournamentSpriteText { - Text = new RomanisableString( - $"{BeatmapInfo.Metadata.ArtistUnicode ?? BeatmapInfo.Metadata.Artist} - {BeatmapInfo.Metadata.TitleUnicode ?? BeatmapInfo.Metadata.Title}", - $"{BeatmapInfo.Metadata.Artist} - {BeatmapInfo.Metadata.Title}"), + Text = BeatmapInfo.GetDisplayTitleRomanisable(false), Font = OsuFont.Torus.With(weight: FontWeight.Bold), }, new FillFlowContainer @@ -93,7 +92,7 @@ namespace osu.Game.Tournament.Components }, new TournamentSpriteText { - Text = BeatmapInfo.Metadata.AuthorString, + Text = BeatmapInfo.Metadata?.Author, Padding = new MarginPadding { Right = 20 }, Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) }, @@ -105,7 +104,7 @@ namespace osu.Game.Tournament.Components }, new TournamentSpriteText { - Text = BeatmapInfo.Version, + Text = BeatmapInfo.DifficultyName, Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) }, } @@ -149,7 +148,7 @@ namespace osu.Game.Tournament.Components private void updateState() { - var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == BeatmapInfo.OnlineBeatmapID); + var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == BeatmapInfo.OnlineID); bool doFlash = found != choice; choice = found; diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 1e3c550323..5f6546c303 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -147,11 +147,11 @@ namespace osu.Game.Tournament.Screens.MapPool if (map != null) { - if (e.Button == MouseButton.Left && map.BeatmapInfo.OnlineBeatmapID != null) - addForBeatmap(map.BeatmapInfo.OnlineBeatmapID.Value); + if (e.Button == MouseButton.Left && map.BeatmapInfo.OnlineID > 0) + addForBeatmap(map.BeatmapInfo.OnlineID); else { - var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.BeatmapInfo.OnlineBeatmapID); + var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.BeatmapInfo.OnlineID); if (existing != null) { diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index eba19ac1a1..836302c424 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -16,12 +16,17 @@ namespace osu.Game.Beatmaps /// /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. /// - public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapInfo beatmapInfo) + public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapInfo beatmapInfo, bool includeDifficultyName = true) { var metadata = getClosestMetadata(beatmapInfo).GetDisplayTitleRomanisable(); - var versionString = getVersionString(beatmapInfo); - return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); + if (includeDifficultyName) + { + var versionString = getVersionString(beatmapInfo); + return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); + } + + return new RomanisableString($"{metadata.GetPreferred(true)}".Trim(), $"{metadata.GetPreferred(false)}".Trim()); } public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[] diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index e8c77e792f..c3e2399d53 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -6,13 +6,15 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using JetBrains.Annotations; +using Newtonsoft.Json; using osu.Framework.Testing; using osu.Game.Database; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] - public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete, IEquatable, IBeatmapSetInfo + public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete, IEquatable, IBeatmapSetInfo, IBeatmapSetOnlineInfo { public int ID { get; set; } @@ -26,8 +28,6 @@ namespace osu.Game.Beatmaps public DateTimeOffset DateAdded { get; set; } - public BeatmapSetOnlineStatus Status { get; set; } = BeatmapSetOnlineStatus.None; - public BeatmapMetadata Metadata { get; set; } public List Beatmaps { get; set; } @@ -36,7 +36,7 @@ namespace osu.Game.Beatmaps public List Files { get; set; } = new List(); [NotMapped] - public BeatmapSetOnlineInfo OnlineInfo { get; set; } + public APIBeatmapSet OnlineInfo { get; set; } [NotMapped] public BeatmapSetMetrics Metrics { get; set; } @@ -102,5 +102,141 @@ namespace osu.Game.Beatmaps IEnumerable IBeatmapSetInfo.Files => Files; #endregion + + #region Delegation for IBeatmapSetOnlineInfo + + [NotMapped] + [JsonIgnore] + public DateTimeOffset Submitted + { + get => OnlineInfo.Submitted; + set => OnlineInfo.Submitted = value; + } + + [NotMapped] + [JsonIgnore] + public DateTimeOffset? Ranked + { + get => OnlineInfo.Ranked; + set => OnlineInfo.Ranked = value; + } + + [NotMapped] + [JsonIgnore] + public DateTimeOffset? LastUpdated + { + get => OnlineInfo.LastUpdated; + set => OnlineInfo.LastUpdated = value; + } + + [NotMapped] + [JsonIgnore] + public BeatmapSetOnlineStatus Status { get; set; } = BeatmapSetOnlineStatus.None; + + [NotMapped] + [JsonIgnore] + public bool HasExplicitContent + { + get => OnlineInfo.HasExplicitContent; + set => OnlineInfo.HasExplicitContent = value; + } + + [NotMapped] + [JsonIgnore] + public bool HasVideo + { + get => OnlineInfo.HasVideo; + set => OnlineInfo.HasVideo = value; + } + + [NotMapped] + [JsonIgnore] + public bool HasStoryboard + { + get => OnlineInfo.HasStoryboard; + set => OnlineInfo.HasStoryboard = value; + } + + [NotMapped] + [JsonIgnore] + public BeatmapSetOnlineCovers Covers + { + get => OnlineInfo.Covers; + set => OnlineInfo.Covers = value; + } + + [NotMapped] + [JsonIgnore] + public string Preview + { + get => OnlineInfo.Preview; + set => OnlineInfo.Preview = value; + } + + [NotMapped] + [JsonIgnore] + public double BPM + { + get => OnlineInfo.BPM; + set => OnlineInfo.BPM = value; + } + + [NotMapped] + [JsonIgnore] + public int PlayCount + { + get => OnlineInfo.PlayCount; + set => OnlineInfo.PlayCount = value; + } + + [NotMapped] + [JsonIgnore] + public int FavouriteCount + { + get => OnlineInfo.FavouriteCount; + set => OnlineInfo.FavouriteCount = value; + } + + [NotMapped] + [JsonIgnore] + public bool HasFavourited + { + get => OnlineInfo.HasFavourited; + set => OnlineInfo.HasFavourited = value; + } + + [NotMapped] + [JsonIgnore] + public BeatmapSetOnlineAvailability Availability + { + get => OnlineInfo.Availability; + set => OnlineInfo.Availability = value; + } + + [NotMapped] + [JsonIgnore] + public BeatmapSetOnlineGenre Genre + { + get => OnlineInfo.Genre; + set => OnlineInfo.Genre = value; + } + + [NotMapped] + [JsonIgnore] + public BeatmapSetOnlineLanguage Language + { + get => OnlineInfo.Language; + set => OnlineInfo.Language = value; + } + + [NotMapped] + [JsonIgnore] + public int? TrackId + { + get => OnlineInfo.TrackId; + set => OnlineInfo.TrackId = value; + } + + #endregion } } diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs index 3658dbab83..e51729f273 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs @@ -9,7 +9,7 @@ namespace osu.Game.Beatmaps /// /// Beatmap set info retrieved for previewing locally without having the set downloaded. /// - public class BeatmapSetOnlineInfo + public interface IBeatmapSetOnlineInfo { /// /// The date this beatmap set was submitted to the online listing. diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs index 5245bc319d..50c08ee2a3 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs @@ -11,10 +11,10 @@ namespace osu.Game.Beatmaps.Drawables [LongRunningLoad] public class BeatmapSetCover : Sprite { - private readonly BeatmapSetInfo set; + private readonly IBeatmapSetOnlineInfo set; private readonly BeatmapSetCoverType type; - public BeatmapSetCover(BeatmapSetInfo set, BeatmapSetCoverType type = BeatmapSetCoverType.Cover) + public BeatmapSetCover(IBeatmapSetOnlineInfo set, BeatmapSetCoverType type = BeatmapSetCoverType.Cover) { if (set == null) throw new ArgumentNullException(nameof(set)); @@ -31,15 +31,15 @@ namespace osu.Game.Beatmaps.Drawables switch (type) { case BeatmapSetCoverType.Cover: - resource = set.OnlineInfo.Covers.Cover; + resource = set.Covers.Cover; break; case BeatmapSetCoverType.Card: - resource = set.OnlineInfo.Covers.Card; + resource = set.Covers.Card; break; case BeatmapSetCoverType.List: - resource = set.OnlineInfo.Covers.List; + resource = set.Covers.List; break; } diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs index 7248c9213c..8bfe98c26b 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs @@ -9,11 +9,11 @@ using osu.Game.Graphics; namespace osu.Game.Beatmaps.Drawables { - public class UpdateableBeatmapSetCover : ModelBackedDrawable + public class UpdateableBeatmapSetCover : ModelBackedDrawable { private readonly BeatmapSetCoverType coverType; - public BeatmapSetInfo BeatmapSet + public IBeatmapSetOnlineInfo BeatmapSet { get => Model; set => Model = value; @@ -43,7 +43,7 @@ namespace osu.Game.Beatmaps.Drawables protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad); - protected override Drawable CreateDrawable(BeatmapSetInfo model) + protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model) { if (model == null) return null; diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index 35963792d0..25d69c2797 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -10,10 +10,10 @@ using osu.Game.Rulesets; namespace osu.Game.Online.API.Requests.Responses { - public class APIBeatmapSet : BeatmapMetadata // todo: this is a bit wrong... + public class APIBeatmapSet : BeatmapMetadata, IBeatmapSetOnlineInfo { [JsonProperty(@"covers")] - private BeatmapSetOnlineCovers covers { get; set; } + public BeatmapSetOnlineCovers Covers { get; set; } private int? onlineBeatmapSetID; @@ -28,43 +28,43 @@ namespace osu.Game.Online.API.Requests.Responses public BeatmapSetOnlineStatus Status { get; set; } [JsonProperty(@"preview_url")] - private string preview { get; set; } + public string Preview { get; set; } [JsonProperty(@"has_favourited")] - private bool hasFavourited { get; set; } + public bool HasFavourited { get; set; } [JsonProperty(@"play_count")] - private int playCount { get; set; } + public int PlayCount { get; set; } [JsonProperty(@"favourite_count")] - private int favouriteCount { get; set; } + public int FavouriteCount { get; set; } [JsonProperty(@"bpm")] - private double bpm { get; set; } + public double BPM { get; set; } [JsonProperty(@"nsfw")] - private bool hasExplicitContent { get; set; } + public bool HasExplicitContent { get; set; } [JsonProperty(@"video")] - private bool hasVideo { get; set; } + public bool HasVideo { get; set; } [JsonProperty(@"storyboard")] - private bool hasStoryboard { get; set; } + public bool HasStoryboard { get; set; } [JsonProperty(@"submitted_date")] - private DateTimeOffset submitted { get; set; } + public DateTimeOffset Submitted { get; set; } [JsonProperty(@"ranked_date")] - private DateTimeOffset? ranked { get; set; } + public DateTimeOffset? Ranked { get; set; } [JsonProperty(@"last_updated")] - private DateTimeOffset lastUpdated { get; set; } + public DateTimeOffset? LastUpdated { get; set; } [JsonProperty(@"ratings")] private int[] ratings { get; set; } [JsonProperty(@"track_id")] - private int? trackId { get; set; } + public int? TrackId { get; set; } [JsonProperty(@"user_id")] private int creatorId @@ -73,13 +73,13 @@ namespace osu.Game.Online.API.Requests.Responses } [JsonProperty(@"availability")] - private BeatmapSetOnlineAvailability availability { get; set; } + public BeatmapSetOnlineAvailability Availability { get; set; } [JsonProperty(@"genre")] - private BeatmapSetOnlineGenre genre { get; set; } + public BeatmapSetOnlineGenre Genre { get; set; } [JsonProperty(@"language")] - private BeatmapSetOnlineLanguage language { get; set; } + public BeatmapSetOnlineLanguage Language { get; set; } [JsonProperty(@"beatmaps")] private IEnumerable beatmaps { get; set; } @@ -92,26 +92,7 @@ namespace osu.Game.Online.API.Requests.Responses Metadata = this, Status = Status, Metrics = ratings == null ? null : new BeatmapSetMetrics { Ratings = ratings }, - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = covers, - Preview = preview, - PlayCount = playCount, - FavouriteCount = favouriteCount, - BPM = bpm, - Status = Status, - HasExplicitContent = hasExplicitContent, - HasVideo = hasVideo, - HasStoryboard = hasStoryboard, - Submitted = submitted, - Ranked = ranked, - LastUpdated = lastUpdated, - Availability = availability, - HasFavourited = hasFavourited, - Genre = genre, - Language = language, - TrackId = trackId - }, + OnlineInfo = this }; beatmapSet.Beatmaps = beatmaps?.Select(b => diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 86879ba245..52aa115083 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -59,7 +59,7 @@ namespace osu.Game.Online.Rooms protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet) { - int? beatmapId = SelectedItem.Value?.Beatmap.Value.OnlineBeatmapID; + int beatmapId = SelectedItem.Value?.Beatmap.Value.OnlineID ?? -1; string checksum = SelectedItem.Value?.Beatmap.Value.MD5Hash; var matchingBeatmap = databasedSet.Beatmaps.FirstOrDefault(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); @@ -75,10 +75,10 @@ namespace osu.Game.Online.Rooms protected override bool IsModelAvailableLocally() { - int? beatmapId = SelectedItem.Value.Beatmap.Value.OnlineBeatmapID; + int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID; string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; - var beatmap = Manager.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); + var beatmap = Manager.QueryBeatmap(b => b.OnlineBeatmapID == onlineId && b.MD5Hash == checksum); return beatmap?.BeatmapSet.DeletePending == false; } diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs index e5a5e35897..2901758332 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.OnlinePlay.Components Text = new RomanisableString(beatmap.Value.Metadata.TitleUnicode, beatmap.Value.Metadata.Title), Font = OsuFont.GetFont(size: TextSize), } - }, LinkAction.OpenBeatmap, beatmap.Value.OnlineBeatmapID.ToString(), "Open beatmap"); + }, LinkAction.OpenBeatmap, beatmap.Value.OnlineID.ToString(), "Open beatmap"); } } } diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 2717146c99..d8e72d31a7 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -8,6 +8,7 @@ using System.Text; using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.IO; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using Decoder = osu.Game.Beatmaps.Formats.Decoder; @@ -32,7 +33,7 @@ namespace osu.Game.Tests.Beatmaps BeatmapInfo.BeatmapSet.Beatmaps = new List { BeatmapInfo }; BeatmapInfo.Length = 75000; BeatmapInfo.OnlineInfo = new BeatmapOnlineInfo(); - BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo + BeatmapInfo.BeatmapSet.OnlineInfo = new APIBeatmapSet { Status = BeatmapSetOnlineStatus.Ranked, Covers = new BeatmapSetOnlineCovers From 32d01f022ff618dd17365d7f810e103613339d58 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Oct 2021 16:53:50 +0900 Subject: [PATCH 71/79] Rename usages which rely on online backing --- .../TestSceneUpdateableBeatmapSetCover.cs | 36 +++++++++---------- .../Components/TournamentBeatmapPanel.cs | 2 +- ...apSetCover.cs => OnlineBeatmapSetCover.cs} | 4 +-- .../UpdateableBeatmapBackgroundSprite.cs | 2 +- ....cs => UpdateableOnlineBeatmapSetCover.cs} | 6 ++-- .../BeatmapListingSearchControl.cs | 4 +-- .../BeatmapListing/Panels/BeatmapPanel.cs | 2 +- .../BeatmapSet/BeatmapSetHeaderContent.cs | 4 +-- .../Dashboard/Home/DashboardBeatmapPanel.cs | 2 +- .../Historical/DrawableMostPlayedBeatmap.cs | 2 +- 10 files changed, 32 insertions(+), 32 deletions(-) rename osu.Game/Beatmaps/Drawables/{BeatmapSetCover.cs => OnlineBeatmapSetCover.cs} (89%) rename osu.Game/Beatmaps/Drawables/{UpdateableBeatmapSetCover.cs => UpdateableOnlineBeatmapSetCover.cs} (85%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs index 01e628075c..3ac3002713 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs @@ -23,21 +23,21 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestLocal([Values] BeatmapSetCoverType coverType) { - AddStep("setup cover", () => Child = new UpdateableBeatmapSetCover(coverType) + AddStep("setup cover", () => Child = new UpdateableOnlineBeatmapSetCover(coverType) { BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet, RelativeSizeAxes = Axes.Both, Masking = true, }); - AddUntilStep("wait for load", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded ?? false); + AddUntilStep("wait for load", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded ?? false); } [Test] public void TestUnloadAndReload() { OsuScrollContainer scroll = null; - List covers = new List(); + List covers = new List(); AddStep("setup covers", () => { @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.UserInterface { var coverType = coverTypes[i % coverTypes.Count]; - var cover = new UpdateableBeatmapSetCover(coverType) + var cover = new UpdateableOnlineBeatmapSetCover(coverType) { BeatmapSet = setInfo, Height = 100, @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.UserInterface } }); - var loadedCovers = covers.Where(c => c.ChildrenOfType().SingleOrDefault()?.IsLoaded ?? false); + var loadedCovers = covers.Where(c => c.ChildrenOfType().SingleOrDefault()?.IsLoaded ?? false); AddUntilStep("some loaded", () => loadedCovers.Any()); AddStep("scroll to end", () => scroll.ScrollToEnd()); @@ -95,9 +95,9 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestSetNullBeatmapWhileLoading() { - TestUpdateableBeatmapSetCover updateableCover = null; + TestUpdateableOnlineBeatmapSetCover updateableCover = null; - AddStep("setup cover", () => Child = updateableCover = new TestUpdateableBeatmapSetCover + AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover { BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet, RelativeSizeAxes = Axes.Both, @@ -112,10 +112,10 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestCoverChangeOnNewBeatmap() { - TestUpdateableBeatmapSetCover updateableCover = null; - BeatmapSetCover initialCover = null; + TestUpdateableOnlineBeatmapSetCover updateableCover = null; + OnlineBeatmapSetCover initialCover = null; - AddStep("setup cover", () => Child = updateableCover = new TestUpdateableBeatmapSetCover(0) + AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover(0) { BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg"), RelativeSizeAxes = Axes.Both, @@ -123,13 +123,13 @@ namespace osu.Game.Tests.Visual.UserInterface Alpha = 0.4f }); - AddUntilStep("cover loaded", () => updateableCover.ChildrenOfType().Any()); - AddStep("store initial cover", () => initialCover = updateableCover.ChildrenOfType().Single()); + AddUntilStep("cover loaded", () => updateableCover.ChildrenOfType().Any()); + AddStep("store initial cover", () => initialCover = updateableCover.ChildrenOfType().Single()); AddUntilStep("wait for fade complete", () => initialCover.Alpha == 1); AddStep("switch beatmap", () => updateableCover.BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg")); - AddUntilStep("new cover loaded", () => updateableCover.ChildrenOfType().Except(new[] { initialCover }).Any()); + AddUntilStep("new cover loaded", () => updateableCover.ChildrenOfType().Except(new[] { initialCover }).Any()); } private static BeatmapSetInfo createBeatmapWithCover(string coverUrl) => new BeatmapSetInfo @@ -140,11 +140,11 @@ namespace osu.Game.Tests.Visual.UserInterface } }; - private class TestUpdateableBeatmapSetCover : UpdateableBeatmapSetCover + private class TestUpdateableOnlineBeatmapSetCover : UpdateableOnlineBeatmapSetCover { private readonly int loadDelay; - public TestUpdateableBeatmapSetCover(int loadDelay = 10000) + public TestUpdateableOnlineBeatmapSetCover(int loadDelay = 10000) { this.loadDelay = loadDelay; } @@ -154,7 +154,7 @@ namespace osu.Game.Tests.Visual.UserInterface if (model == null) return null; - return new TestBeatmapSetCover(model, loadDelay) + return new TestOnlineBeatmapSetCover(model, loadDelay) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -164,11 +164,11 @@ namespace osu.Game.Tests.Visual.UserInterface } } - private class TestBeatmapSetCover : BeatmapSetCover + private class TestOnlineBeatmapSetCover : OnlineBeatmapSetCover { private readonly int loadDelay; - public TestBeatmapSetCover(IBeatmapSetOnlineInfo set, int loadDelay) + public TestOnlineBeatmapSetCover(IBeatmapSetOnlineInfo set, int loadDelay) : base(set) { this.loadDelay = loadDelay; diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index a31430495d..be29566e07 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tournament.Components RelativeSizeAxes = Axes.Both, Colour = Color4.Black, }, - new UpdateableBeatmapSetCover + new UpdateableOnlineBeatmapSetCover { RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(0.5f), diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/OnlineBeatmapSetCover.cs similarity index 89% rename from osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs rename to osu.Game/Beatmaps/Drawables/OnlineBeatmapSetCover.cs index 50c08ee2a3..0b19c27022 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs +++ b/osu.Game/Beatmaps/Drawables/OnlineBeatmapSetCover.cs @@ -9,12 +9,12 @@ using osu.Framework.Graphics.Textures; namespace osu.Game.Beatmaps.Drawables { [LongRunningLoad] - public class BeatmapSetCover : Sprite + public class OnlineBeatmapSetCover : Sprite { private readonly IBeatmapSetOnlineInfo set; private readonly BeatmapSetCoverType type; - public BeatmapSetCover(IBeatmapSetOnlineInfo set, BeatmapSetCoverType type = BeatmapSetCoverType.Cover) + public OnlineBeatmapSetCover(IBeatmapSetOnlineInfo set, BeatmapSetCoverType type = BeatmapSetCoverType.Cover) { if (set == null) throw new ArgumentNullException(nameof(set)); diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs index 3206f7b3ab..8943ad350e 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs @@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps.Drawables { // prefer online cover where available. if (model?.BeatmapSet?.OnlineInfo != null) - return new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType); + return new OnlineBeatmapSetCover(model.BeatmapSet, beatmapSetCoverType); return model?.ID > 0 ? new BeatmapBackgroundSprite(beatmaps.GetWorkingBeatmap(model)) diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/UpdateableOnlineBeatmapSetCover.cs similarity index 85% rename from osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs rename to osu.Game/Beatmaps/Drawables/UpdateableOnlineBeatmapSetCover.cs index 8bfe98c26b..73f87beb58 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableOnlineBeatmapSetCover.cs @@ -9,7 +9,7 @@ using osu.Game.Graphics; namespace osu.Game.Beatmaps.Drawables { - public class UpdateableBeatmapSetCover : ModelBackedDrawable + public class UpdateableOnlineBeatmapSetCover : ModelBackedDrawable { private readonly BeatmapSetCoverType coverType; @@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.Drawables set => base.Masking = value; } - public UpdateableBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover) + public UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover) { this.coverType = coverType; @@ -48,7 +48,7 @@ namespace osu.Game.Beatmaps.Drawables if (model == null) return null; - return new BeatmapSetCover(model, coverType) + return new OnlineBeatmapSetCover(model, coverType) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index bbde03aa10..da2dcfebdf 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -76,7 +76,7 @@ namespace osu.Game.Overlays.BeatmapListing private readonly BeatmapSearchFilterRow explicitContentFilter; private readonly Box background; - private readonly UpdateableBeatmapSetCover beatmapCover; + private readonly UpdateableOnlineBeatmapSetCover beatmapCover; public BeatmapListingSearchControl() { @@ -196,7 +196,7 @@ namespace osu.Game.Overlays.BeatmapListing } } - private class TopSearchBeatmapSetCover : UpdateableBeatmapSetCover + private class TopSearchBeatmapSetCover : UpdateableOnlineBeatmapSetCover { protected override bool TransformImmediately => true; } diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs index 9ff39ce1dd..779f3860f2 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs @@ -160,7 +160,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels return icons; } - protected Drawable CreateBackground() => new UpdateableBeatmapSetCover + protected Drawable CreateBackground() => new UpdateableOnlineBeatmapSetCover { RelativeSizeAxes = Axes.Both, BeatmapSet = SetInfo, diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index dcf06ac7fb..fc2f90807e 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.BeatmapSet public readonly Details Details; public readonly BeatmapPicker Picker; - private readonly UpdateableBeatmapSetCover cover; + private readonly UpdateableOnlineBeatmapSetCover cover; private readonly Box coverGradient; private readonly OsuSpriteText title, artist; private readonly AuthorInfo author; @@ -68,7 +68,7 @@ namespace osu.Game.Overlays.BeatmapSet RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - cover = new UpdateableBeatmapSetCover + cover = new UpdateableOnlineBeatmapSetCover { RelativeSizeAxes = Axes.Both, Masking = true, diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs index 3badea155d..edc737d8fe 100644 --- a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Dashboard.Home RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = 6, - Child = new UpdateableBeatmapSetCover + Child = new UpdateableOnlineBeatmapSetCover { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index 2c6fa76ca4..32201e36a9 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { AddRangeInternal(new Drawable[] { - new UpdateableBeatmapSetCover(BeatmapSetCoverType.List) + new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Y, Width = cover_width, From b73bd54ab2bbd47ed105d418182da780f6b333d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Oct 2021 16:54:32 +0900 Subject: [PATCH 72/79] Split out individual pieces into own files --- .../Beatmaps/BeatmapSetOnlineAvailability.cs | 16 +++++++ osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs | 25 +++++++++++ osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs | 11 +++++ osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs | 11 +++++ ...OnlineInfo.cs => IBeatmapSetOnlineInfo.cs} | 45 +------------------ 5 files changed, 64 insertions(+), 44 deletions(-) create mode 100644 osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs create mode 100644 osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs create mode 100644 osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs create mode 100644 osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs rename osu.Game/Beatmaps/{BeatmapSetOnlineInfo.cs => IBeatmapSetOnlineInfo.cs} (72%) diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs b/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs new file mode 100644 index 0000000000..8c67aa2404 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Beatmaps +{ + public class BeatmapSetOnlineAvailability + { + [JsonProperty(@"download_disabled")] + public bool DownloadDisabled { get; set; } + + [JsonProperty(@"more_information")] + public string ExternalLink { get; set; } + } +} diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs b/osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs new file mode 100644 index 0000000000..8d6e85391d --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Beatmaps +{ + public class BeatmapSetOnlineCovers + { + public string CoverLowRes { get; set; } + + [JsonProperty(@"cover@2x")] + public string Cover { get; set; } + + public string CardLowRes { get; set; } + + [JsonProperty(@"card@2x")] + public string Card { get; set; } + + public string ListLowRes { get; set; } + + [JsonProperty(@"list@2x")] + public string List { get; set; } + } +} diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs b/osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs new file mode 100644 index 0000000000..65d6dfe5d9 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Beatmaps +{ + public class BeatmapSetOnlineGenre + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs b/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs new file mode 100644 index 0000000000..057325f319 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Beatmaps +{ + public class BeatmapSetOnlineLanguage + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs similarity index 72% rename from osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs rename to osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs index e51729f273..7f1ba41fb6 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs @@ -1,8 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - using System; -using Newtonsoft.Json; namespace osu.Game.Beatmaps { @@ -97,43 +93,4 @@ namespace osu.Game.Beatmaps /// public int? TrackId { get; set; } } - - public class BeatmapSetOnlineGenre - { - public int Id { get; set; } - public string Name { get; set; } - } - - public class BeatmapSetOnlineLanguage - { - public int Id { get; set; } - public string Name { get; set; } - } - - public class BeatmapSetOnlineCovers - { - public string CoverLowRes { get; set; } - - [JsonProperty(@"cover@2x")] - public string Cover { get; set; } - - public string CardLowRes { get; set; } - - [JsonProperty(@"card@2x")] - public string Card { get; set; } - - public string ListLowRes { get; set; } - - [JsonProperty(@"list@2x")] - public string List { get; set; } - } - - public class BeatmapSetOnlineAvailability - { - [JsonProperty(@"download_disabled")] - public bool DownloadDisabled { get; set; } - - [JsonProperty(@"more_information")] - public string ExternalLink { get; set; } - } -} +} \ No newline at end of file From ff674ca91378f70705b3e0ab38f2f027a4cdf225 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Oct 2021 17:00:14 +0900 Subject: [PATCH 73/79] Remove unnecessary access modifiers from interface --- osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs | 36 +++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs index 7f1ba41fb6..86859a0dc3 100644 --- a/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs @@ -10,87 +10,87 @@ namespace osu.Game.Beatmaps /// /// The date this beatmap set was submitted to the online listing. /// - public DateTimeOffset Submitted { get; set; } + DateTimeOffset Submitted { get; set; } /// /// The date this beatmap set was ranked. /// - public DateTimeOffset? Ranked { get; set; } + DateTimeOffset? Ranked { get; set; } /// /// The date this beatmap set was last updated. /// - public DateTimeOffset? LastUpdated { get; set; } + DateTimeOffset? LastUpdated { get; set; } /// /// The status of this beatmap set. /// - public BeatmapSetOnlineStatus Status { get; set; } + BeatmapSetOnlineStatus Status { get; set; } /// /// Whether or not this beatmap set has explicit content. /// - public bool HasExplicitContent { get; set; } + bool HasExplicitContent { get; set; } /// /// Whether or not this beatmap set has a background video. /// - public bool HasVideo { get; set; } + bool HasVideo { get; set; } /// /// Whether or not this beatmap set has a storyboard. /// - public bool HasStoryboard { get; set; } + bool HasStoryboard { get; set; } /// /// The different sizes of cover art for this beatmap set. /// - public BeatmapSetOnlineCovers Covers { get; set; } + BeatmapSetOnlineCovers Covers { get; set; } /// /// A small sample clip of this beatmap set's song. /// - public string Preview { get; set; } + string Preview { get; set; } /// /// The beats per minute of this beatmap set's song. /// - public double BPM { get; set; } + double BPM { get; set; } /// /// The amount of plays this beatmap set has. /// - public int PlayCount { get; set; } + int PlayCount { get; set; } /// /// The amount of people who have favourited this beatmap set. /// - public int FavouriteCount { get; set; } + int FavouriteCount { get; set; } /// /// Whether this beatmap set has been favourited by the current user. /// - public bool HasFavourited { get; set; } + bool HasFavourited { get; set; } /// /// The availability of this beatmap set. /// - public BeatmapSetOnlineAvailability Availability { get; set; } + BeatmapSetOnlineAvailability Availability { get; set; } /// /// The song genre of this beatmap set. /// - public BeatmapSetOnlineGenre Genre { get; set; } + BeatmapSetOnlineGenre Genre { get; set; } /// /// The song language of this beatmap set. /// - public BeatmapSetOnlineLanguage Language { get; set; } + BeatmapSetOnlineLanguage Language { get; set; } /// /// The track ID of this beatmap set. /// Non-null only if the track is linked to a featured artist track entry. /// - public int? TrackId { get; set; } + int? TrackId { get; set; } } -} \ No newline at end of file +} From 0335ed6f276946b9ec9194e32f66b35ce7586d1f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Oct 2021 17:14:29 +0900 Subject: [PATCH 74/79] Add missing licence header --- osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs index 86859a0dc3..f365a59c1b 100644 --- a/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System; namespace osu.Game.Beatmaps From 69e7810dad46f6529fd2753479b5a39fe318fc60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Oct 2021 18:53:49 +0900 Subject: [PATCH 75/79] Enable `nullable` and switch classes to structs --- osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs | 2 +- osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs | 2 +- osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs | 2 +- osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs | 4 +++- .../BeatmapListing/Panels/BeatmapPanelDownloadButton.cs | 2 +- osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs | 4 ++-- osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs | 2 +- osu.Game/Overlays/BeatmapSet/Info.cs | 4 ++-- .../Sections/Beatmaps/PaginatedBeatmapContainer.cs | 8 ++++---- .../OnlinePlay/Components/PlaylistItemBackground.cs | 2 +- 10 files changed, 17 insertions(+), 15 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs b/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs index 8c67aa2404..14a63f3279 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs @@ -5,7 +5,7 @@ using Newtonsoft.Json; namespace osu.Game.Beatmaps { - public class BeatmapSetOnlineAvailability + public struct BeatmapSetOnlineAvailability { [JsonProperty(@"download_disabled")] public bool DownloadDisabled { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs b/osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs index 65d6dfe5d9..e727e2c37f 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs @@ -3,7 +3,7 @@ namespace osu.Game.Beatmaps { - public class BeatmapSetOnlineGenre + public struct BeatmapSetOnlineGenre { public int Id { get; set; } public string Name { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs b/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs index 057325f319..658e5a4005 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs @@ -3,7 +3,7 @@ namespace osu.Game.Beatmaps { - public class BeatmapSetOnlineLanguage + public struct BeatmapSetOnlineLanguage { public int Id { get; set; } public string Name { get; set; } diff --git a/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs index f365a59c1b..1cd363f7b6 100644 --- a/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs @@ -3,6 +3,8 @@ using System; +#nullable enable + namespace osu.Game.Beatmaps { /// @@ -48,7 +50,7 @@ namespace osu.Game.Beatmaps /// /// The different sizes of cover art for this beatmap set. /// - BeatmapSetOnlineCovers Covers { get; set; } + BeatmapSetOnlineCovers? Covers { get; set; } /// /// A small sample clip of this beatmap set's song. diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs index 47b477ef9a..a8c4334ffb 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs @@ -92,7 +92,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels break; default: - if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false) + if (BeatmapSet.Value?.OnlineInfo?.Availability.DownloadDisabled ?? false) { button.Enabled.Value = false; button.TooltipText = "this beatmap is currently not available for download."; diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs b/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs index 896c646552..f005a37eaa 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapAvailability.cs @@ -16,8 +16,8 @@ namespace osu.Game.Overlays.BeatmapSet { private BeatmapSetInfo beatmapSet; - private bool downloadDisabled => BeatmapSet?.OnlineInfo.Availability?.DownloadDisabled ?? false; - private bool hasExternalLink => !string.IsNullOrEmpty(BeatmapSet?.OnlineInfo.Availability?.ExternalLink); + private bool downloadDisabled => BeatmapSet?.OnlineInfo.Availability.DownloadDisabled ?? false; + private bool hasExternalLink => !string.IsNullOrEmpty(BeatmapSet?.OnlineInfo.Availability.ExternalLink); private readonly LinkFlowContainer textContainer; diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index fc2f90807e..c1029923f7 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -266,7 +266,7 @@ namespace osu.Game.Overlays.BeatmapSet { if (BeatmapSet.Value == null) return; - if ((BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) && State.Value != DownloadState.LocallyAvailable) + if (BeatmapSet.Value.OnlineInfo.Availability.DownloadDisabled && State.Value != DownloadState.LocallyAvailable) { downloadButtonsContainer.Clear(); return; diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index 61c660cbaa..8bc5c6d27e 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.cs @@ -117,8 +117,8 @@ namespace osu.Game.Overlays.BeatmapSet { source.Text = b.NewValue?.Metadata.Source ?? string.Empty; tags.Text = b.NewValue?.Metadata.Tags ?? string.Empty; - genre.Text = b.NewValue?.OnlineInfo?.Genre?.Name ?? string.Empty; - language.Text = b.NewValue?.OnlineInfo?.Language?.Name ?? string.Empty; + genre.Text = b.NewValue?.OnlineInfo?.Genre.Name ?? string.Empty; + language.Text = b.NewValue?.OnlineInfo?.Language.Name ?? string.Empty; var setHasLeaderboard = b.NewValue?.OnlineInfo?.Status > 0; successRate.Alpha = setHasLeaderboard ? 1 : 0; notRankedPlaceholder.Alpha = setHasLeaderboard ? 0 : 1; diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 8657e356c9..c1e56facd9 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -60,12 +60,12 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps protected override APIRequest> CreateRequest() => new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); - protected override Drawable CreateDrawableItem(APIBeatmapSet model) => !model.OnlineBeatmapSetID.HasValue - ? null - : new GridBeatmapPanel(model.ToBeatmapSet(Rulesets)) + protected override Drawable CreateDrawableItem(APIBeatmapSet model) => model.OnlineID > 0 + ? new GridBeatmapPanel(model.ToBeatmapSet(Rulesets)) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - }; + } + : null; } } diff --git a/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs b/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs index 90ad6e0f6e..c2ceef292c 100644 --- a/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs +++ b/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.OnlinePlay.Components Texture? texture = null; // prefer online cover where available. - if (BeatmapInfo?.BeatmapSet?.OnlineInfo?.Covers.Cover != null) + if (BeatmapInfo?.BeatmapSet?.OnlineInfo?.Covers?.Cover != null) texture = textures.Get(BeatmapInfo.BeatmapSet.OnlineInfo.Covers.Cover); Sprite.Texture = texture ?? beatmaps.DefaultBeatmap.Background; From 40a176e86e6ef829c43a25b4343850966374b5e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Oct 2021 18:54:21 +0900 Subject: [PATCH 76/79] `APIBeatmapSet` implements `IBeatmapSetInfo` --- .../API/Requests/Responses/APIBeatmapSet.cs | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index 25d69c2797..c445a69126 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -6,29 +6,26 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Rulesets; +#nullable enable + namespace osu.Game.Online.API.Requests.Responses { - public class APIBeatmapSet : BeatmapMetadata, IBeatmapSetOnlineInfo + public class APIBeatmapSet : BeatmapMetadata, IBeatmapSetOnlineInfo, IBeatmapSetInfo { [JsonProperty(@"covers")] - public BeatmapSetOnlineCovers Covers { get; set; } - - private int? onlineBeatmapSetID; + public BeatmapSetOnlineCovers? Covers { get; set; } [JsonProperty(@"id")] - public int? OnlineBeatmapSetID - { - get => onlineBeatmapSetID; - set => onlineBeatmapSetID = value > 0 ? value : null; - } + public int OnlineID { get; set; } [JsonProperty(@"status")] public BeatmapSetOnlineStatus Status { get; set; } [JsonProperty(@"preview_url")] - public string Preview { get; set; } + public string Preview { get; set; } = string.Empty; [JsonProperty(@"has_favourited")] public bool HasFavourited { get; set; } @@ -61,7 +58,7 @@ namespace osu.Game.Online.API.Requests.Responses public DateTimeOffset? LastUpdated { get; set; } [JsonProperty(@"ratings")] - private int[] ratings { get; set; } + private int[] ratings { get; set; } = Array.Empty(); [JsonProperty(@"track_id")] public int? TrackId { get; set; } @@ -82,20 +79,20 @@ namespace osu.Game.Online.API.Requests.Responses public BeatmapSetOnlineLanguage Language { get; set; } [JsonProperty(@"beatmaps")] - private IEnumerable beatmaps { get; set; } + private IEnumerable beatmaps { get; set; } = Array.Empty(); public virtual BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) { var beatmapSet = new BeatmapSetInfo { - OnlineBeatmapSetID = OnlineBeatmapSetID, + OnlineBeatmapSetID = OnlineID, Metadata = this, Status = Status, - Metrics = ratings == null ? null : new BeatmapSetMetrics { Ratings = ratings }, + Metrics = new BeatmapSetMetrics { Ratings = ratings }, OnlineInfo = this }; - beatmapSet.Beatmaps = beatmaps?.Select(b => + beatmapSet.Beatmaps = beatmaps.Select(b => { var beatmap = b.ToBeatmapInfo(rulesets); beatmap.BeatmapSet = beatmapSet; @@ -105,5 +102,19 @@ namespace osu.Game.Online.API.Requests.Responses return beatmapSet; } + + #region Implementation of IBeatmapSetInfo + + IEnumerable IBeatmapSetInfo.Beatmaps => beatmaps; + + IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => this; + + DateTimeOffset IBeatmapSetInfo.DateAdded => throw new NotImplementedException(); + IEnumerable IBeatmapSetInfo.Files => throw new NotImplementedException(); + double IBeatmapSetInfo.MaxStarDifficulty => throw new NotImplementedException(); + double IBeatmapSetInfo.MaxLength => throw new NotImplementedException(); + double IBeatmapSetInfo.MaxBPM => throw new NotImplementedException(); + + #endregion } } From 0fe0b5dc0999965f2a01e41aae0e703e5edf917b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Oct 2021 19:14:31 +0900 Subject: [PATCH 77/79] `APIBeatmap` implements `IBeatmapInfo` --- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 6 +-- .../API/Requests/Responses/APIBeatmap.cs | 53 ++++++++++++++----- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index 1fe120557d..b05ad9a1dd 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -83,9 +83,9 @@ namespace osu.Game.Beatmaps if (res != null) { beatmapInfo.Status = res.Status; - beatmapInfo.BeatmapSet.Status = res.BeatmapSet.Status; + beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapSetOnlineStatus.None; beatmapInfo.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; - beatmapInfo.OnlineBeatmapID = res.OnlineBeatmapID; + beatmapInfo.OnlineBeatmapID = res.OnlineID; if (beatmapInfo.Metadata != null) beatmapInfo.Metadata.AuthorID = res.AuthorID; @@ -93,7 +93,7 @@ namespace osu.Game.Beatmaps if (beatmapInfo.BeatmapSet.Metadata != null) beatmapInfo.BeatmapSet.Metadata.AuthorID = res.AuthorID; - logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); + logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}."); } } catch (Exception e) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index ea4265e641..42e519223b 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -6,12 +6,14 @@ using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Rulesets; +#nullable enable + namespace osu.Game.Online.API.Requests.Responses { - public class APIBeatmap : BeatmapMetadata + public class APIBeatmap : BeatmapMetadata, IBeatmapInfo { [JsonProperty(@"id")] - public int OnlineBeatmapID { get; set; } + public int OnlineID { get; set; } [JsonProperty(@"beatmapset_id")] public int OnlineBeatmapSetID { get; set; } @@ -20,10 +22,10 @@ namespace osu.Game.Online.API.Requests.Responses public BeatmapSetOnlineStatus Status { get; set; } [JsonProperty("checksum")] - public string Checksum { get; set; } + public string Checksum { get; set; } = string.Empty; [JsonProperty(@"beatmapset")] - public APIBeatmapSet BeatmapSet { get; set; } + public APIBeatmapSet? BeatmapSet { get; set; } [JsonProperty(@"playcount")] private int playCount { get; set; } @@ -32,10 +34,10 @@ namespace osu.Game.Online.API.Requests.Responses private int passCount { get; set; } [JsonProperty(@"mode_int")] - private int ruleset { get; set; } + public int RulesetID { get; set; } [JsonProperty(@"difficulty_rating")] - private double starDifficulty { get; set; } + public double StarRating { get; set; } [JsonProperty(@"drain")] private float drainRate { get; set; } @@ -50,7 +52,7 @@ namespace osu.Game.Online.API.Requests.Responses private float overallDifficulty { get; set; } [JsonProperty(@"total_length")] - private double length { get; set; } + public double Length { get; set; } [JsonProperty(@"count_circles")] private int circleCount { get; set; } @@ -59,10 +61,10 @@ namespace osu.Game.Online.API.Requests.Responses private int sliderCount { get; set; } [JsonProperty(@"version")] - private string version { get; set; } + public string DifficultyName { get; set; } = string.Empty; [JsonProperty(@"failtimes")] - private BeatmapMetrics metrics { get; set; } + private BeatmapMetrics? metrics { get; set; } [JsonProperty(@"max_combo")] private int? maxCombo { get; set; } @@ -74,12 +76,12 @@ namespace osu.Game.Online.API.Requests.Responses return new BeatmapInfo { Metadata = set?.Metadata ?? this, - Ruleset = rulesets.GetRuleset(ruleset), - StarDifficulty = starDifficulty, - OnlineBeatmapID = OnlineBeatmapID, - Version = version, + Ruleset = rulesets.GetRuleset(RulesetID), + StarDifficulty = StarRating, + OnlineBeatmapID = OnlineID, + Version = DifficultyName, // this is actually an incorrect mapping (Length is calculated as drain length in lazer's import process, see BeatmapManager.calculateLength). - Length = TimeSpan.FromSeconds(length).TotalMilliseconds, + Length = TimeSpan.FromSeconds(Length).TotalMilliseconds, Status = Status, MD5Hash = Checksum, BeatmapSet = set, @@ -101,5 +103,28 @@ namespace osu.Game.Online.API.Requests.Responses }, }; } + + #region Implementation of IBeatmapInfo + + public IBeatmapMetadataInfo Metadata => this; + + public IBeatmapDifficultyInfo Difficulty => new BeatmapDifficulty + { + DrainRate = drainRate, + CircleSize = circleSize, + ApproachRate = approachRate, + OverallDifficulty = overallDifficulty, + }; + + IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet; + + public string MD5Hash => Checksum; + + public IRulesetInfo Ruleset => new RulesetInfo { ID = RulesetID }; + + public double BPM => throw new NotImplementedException(); + public string Hash => throw new NotImplementedException(); + + #endregion } } From c580ec865f5c40fcf3548767350ef686bf30f1ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Oct 2021 19:34:01 +0900 Subject: [PATCH 78/79] `APIBeatmapSet.Covers` is never null --- osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs | 2 +- osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs | 2 +- osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs | 2 +- .../Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs | 2 +- .../Screens/OnlinePlay/Components/PlaylistItemBackground.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs b/osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs index 8d6e85391d..aad31befa8 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs @@ -5,7 +5,7 @@ using Newtonsoft.Json; namespace osu.Game.Beatmaps { - public class BeatmapSetOnlineCovers + public struct BeatmapSetOnlineCovers { public string CoverLowRes { get; set; } diff --git a/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs index 1cd363f7b6..b9800bc2e6 100644 --- a/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs @@ -50,7 +50,7 @@ namespace osu.Game.Beatmaps /// /// The different sizes of cover art for this beatmap set. /// - BeatmapSetOnlineCovers? Covers { get; set; } + BeatmapSetOnlineCovers Covers { get; set; } /// /// A small sample clip of this beatmap set's song. diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index c445a69126..47f880cf54 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -16,7 +16,7 @@ namespace osu.Game.Online.API.Requests.Responses public class APIBeatmapSet : BeatmapMetadata, IBeatmapSetOnlineInfo, IBeatmapSetInfo { [JsonProperty(@"covers")] - public BeatmapSetOnlineCovers? Covers { get; set; } + public BeatmapSetOnlineCovers Covers { get; set; } [JsonProperty(@"id")] public int OnlineID { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs index 6ce5f6a6db..8b6077b9f2 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { var beatmap = playlistItem?.Beatmap.Value; - if (background?.BeatmapInfo?.BeatmapSet?.OnlineInfo?.Covers?.Cover == beatmap?.BeatmapSet?.OnlineInfo?.Covers?.Cover) + if (background?.BeatmapInfo?.BeatmapSet?.OnlineInfo?.Covers.Cover == beatmap?.BeatmapSet?.OnlineInfo?.Covers.Cover) return; cancellationSource?.Cancel(); diff --git a/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs b/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs index c2ceef292c..90ad6e0f6e 100644 --- a/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs +++ b/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.OnlinePlay.Components Texture? texture = null; // prefer online cover where available. - if (BeatmapInfo?.BeatmapSet?.OnlineInfo?.Covers?.Cover != null) + if (BeatmapInfo?.BeatmapSet?.OnlineInfo?.Covers.Cover != null) texture = textures.Get(BeatmapInfo.BeatmapSet.OnlineInfo.Covers.Cover); Sprite.Texture = texture ?? beatmaps.DefaultBeatmap.Background; From cb605f91568272a5e0b30b18980661e4795d8d72 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Thu, 21 Oct 2021 16:00:57 +0000 Subject: [PATCH 79/79] removed ppCalc changes and sliderabuseChecks --- .../Difficulty/OsuPerformanceCalculator.cs | 77 ++++--------------- .../Preprocessing/OsuDifficultyHitObject.cs | 24 +++--- .../Difficulty/Skills/Aim.cs | 2 +- 3 files changed, 27 insertions(+), 76 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index f97f4899de..bf4d92652c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -25,8 +25,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int countMeh; private int countMiss; - private int effectiveMissCount; - public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) : base(ruleset, attributes, score) { @@ -41,23 +39,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok); countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); - effectiveMissCount = calculateEffectiveMissCount(); - - double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. // Custom multipliers for NoFail and SpunOut. + double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. + if (mods.Any(m => m is OsuModNoFail)) - multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount); + multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss); if (mods.Any(m => m is OsuModSpunOut)) multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); - if (mods.Any(h => h is OsuModRelax)) - { - effectiveMissCount += countOk + countMeh; - multiplier *= 0.6; - } - double aimValue = computeAimValue(); double speedValue = computeSpeedValue(); double accuracyValue = computeAccuracyValue(); @@ -100,8 +91,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= lengthBonus; // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. - if (effectiveMissCount > 0) - aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount); + if (countMiss > 0) + aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss); // Combo scaling. if (Attributes.MaxCombo > 0) @@ -117,18 +108,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; - if (mods.Any(m => m is OsuModBlinds)) - aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); - else if (mods.Any(h => h is OsuModHidden)) - { - // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. + // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. + if (mods.Any(h => h is OsuModHidden)) aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); - } aimValue *= approachRateBonus; - // Scale the aim value with accuracy - aimValue *= accuracy; + // Scale the aim value with accuracy _slightly_. + aimValue *= 0.5 + accuracy / 2.0; // It is important to also consider accuracy difficulty when doing that. aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; @@ -145,8 +132,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= lengthBonus; // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. - if (effectiveMissCount > 0) - speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); + if (countMiss > 0) + speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875)); // Combo scaling. if (Attributes.MaxCombo > 0) @@ -160,20 +147,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; - if (mods.Any(m => m is OsuModBlinds)) - { - // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given. - speedValue *= 1.12; - } - else if (mods.Any(m => m is OsuModHidden)) - { - // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. + if (mods.Any(m => m is OsuModHidden)) speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); - } // Scale the speed value with accuracy and OD. speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2); - // Scale the speed value with # of 50s to punish doubletapping. speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); @@ -182,9 +160,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAccuracyValue() { - if (mods.Any(h => h is OsuModRelax)) - return 0.0; - // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. double betterAccuracyPercentage; int amountHitObjectsWithAccuracy = Attributes.HitCircleCount; @@ -205,12 +180,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Bonus for many hitcircles - it's harder to keep good accuracy up for longer. accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); - // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. - if (mods.Any(m => m is OsuModBlinds)) - accuracyValue *= 1.14; - else if (mods.Any(m => m is OsuModHidden)) + if (mods.Any(m => m is OsuModHidden)) accuracyValue *= 1.08; - if (mods.Any(m => m is OsuModFlashlight)) accuracyValue *= 1.02; @@ -234,8 +205,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty flashlightValue *= 1.3; // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. - if (effectiveMissCount > 0) - flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); + if (countMiss > 0) + flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875)); // Combo scaling. if (Attributes.MaxCombo > 0) @@ -253,24 +224,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty return flashlightValue; } - private int calculateEffectiveMissCount() - { - // guess the number of misses + slider breaks from combo - double comboBasedMissCount = 0.0; - - if (Attributes.SliderCount > 0) - { - double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount; - if (scoreMaxCombo < fullComboThreshold) - comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); - } - - // we're clamping misscount because since its derived from combo it can be higher than total hits and that breaks some calculations - comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits); - - return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount)); - } - private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index d86358fb57..5d85c4338c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing public class OsuDifficultyHitObject : DifficultyHitObject { private const int normalized_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. + private const int min_delta_time = 25; protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject; @@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing /// public double MovementDistance { get; private set; } - /// JumpTravel + /// /// Normalized distance between the start and end position of the previous . /// public double TravelDistance { get; private set; } @@ -62,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing this.lastObject = (OsuHitObject)lastObject; // Capped to 25ms to prevent difficulty calculation breaking from simulatenous objects. - StrainTime = Math.Max(DeltaTime, 25); + StrainTime = Math.Max(DeltaTime, min_delta_time); setDistances(clockRate); } @@ -82,22 +83,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing scalingFactor *= 1 + smallCircleBonus; } - double sliderAbuseIndex = 1; - if (lastObject is Slider lastSlider) { computeSliderCursorPosition(lastSlider); - sliderAbuseIndex = Math.Clamp(Vector2.Subtract(lastSlider.StackedPosition * scalingFactor, BaseObject.StackedPosition * scalingFactor).Length - 100, 0, 25) / 25; - TravelDistance = lastSlider.LazyTravelDistance * scalingFactor * sliderAbuseIndex; - TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, 25); - MovementTime = Math.Max(StrainTime - TravelTime, 25); + TravelDistance = lastSlider.LazyTravelDistance * scalingFactor; + TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time); + MovementTime = Math.Max(StrainTime - TravelTime, min_delta_time); MovementDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor; } Vector2 lastCursorPosition = getEndCursorPosition(lastObject); - JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length * sliderAbuseIndex; - MovementDistance = Math.Min(JumpDistance, MovementDistance) * sliderAbuseIndex; + JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length; + MovementDistance = Math.Min(JumpDistance, MovementDistance); if (lastLastObject != null && !(lastLastObject is Spinner)) { @@ -120,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing slider.LazyEndPosition = slider.StackedPosition; - float approxFollowCircleRadius = (float)(slider.Radius * 2.4); + float followCircleRadius = (float)(slider.Radius * 2.4); var computeVertex = new Action(t => { double progress = (t - slider.StartTime) / slider.SpanDuration; @@ -135,11 +133,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing slider.LazyTravelTime = t - slider.StartTime; - if (dist > approxFollowCircleRadius) + if (dist > followCircleRadius) { // The cursor would be outside the follow circle, we need to move it diff.Normalize(); // Obtain direction of diff - dist -= approxFollowCircleRadius; + dist -= followCircleRadius; slider.LazyEndPosition += diff * dist; slider.LazyTravelDistance += dist; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 3d27e13bc1..966e7eb221 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills var osuPrevObj = (OsuDifficultyHitObject)Previous[0]; var osuLastObj = (OsuDifficultyHitObject)Previous[1]; - double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime; // Start iwth the base distance / time + double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime; // Start with the base distance / time if (osuPrevObj.BaseObject is Slider) // If object is a slider {