From 8105d4854a53c48617ca7638a989a9442174e69a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jul 2022 13:30:38 +0900 Subject: [PATCH 01/52] Fix beatmap carousel not maintaining selection if currently selected beatmap is updated --- osu.Game/Screens/Select/BeatmapCarousel.cs | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 75caa3c9a3..c6f2b61049 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -265,6 +265,38 @@ namespace osu.Game.Screens.Select foreach (int i in changes.InsertedIndices) UpdateBeatmapSet(sender[i].Detach()); + + if (changes.DeletedIndices.Length > 0) + { + // To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions. + // When an update occurs, the previous beatmap set is either soft or hard deleted. + // Check if the current selection was potentially deleted by re-querying its validity. + bool selectedSetMarkedDeleted = realm.Run(r => r.Find(SelectedBeatmapSet.ID))?.DeletePending != false; + + if (selectedSetMarkedDeleted) + { + // If it is no longer valid, make the bold assumption that an updated version will be available in the modified indices. + // This relies on the full update operation being in a single transaction, so please don't change that. + foreach (int i in changes.NewModifiedIndices) + { + var beatmapSetInfo = sender[i]; + + foreach (var beatmapInfo in beatmapSetInfo.Beatmaps) + { + // Best effort matching. We can't use ID because in the update flow a new version will get its own GUID. + bool selectionMatches = + ((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata) + && beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName; + + if (selectionMatches) + { + SelectBeatmap(beatmapInfo); + break; + } + } + } + } + } } private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) From 24d75612e222fe3e055d1f5fabf623727dc81727 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jul 2022 14:18:53 +0900 Subject: [PATCH 02/52] Always attempt to follow selection, even if difficulty name / metadata change --- osu.Game/Screens/Select/BeatmapCarousel.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index c6f2b61049..0430c15a1d 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -283,18 +283,21 @@ namespace osu.Game.Screens.Select foreach (var beatmapInfo in beatmapSetInfo.Beatmaps) { - // Best effort matching. We can't use ID because in the update flow a new version will get its own GUID. - bool selectionMatches = - ((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata) - && beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName; + if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata)) + continue; - if (selectionMatches) + // Best effort matching. We can't use ID because in the update flow a new version will get its own GUID. + if (beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName) { SelectBeatmap(beatmapInfo); - break; + return; } } } + + // If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed. + // Let's attempt to follow set-level selection anyway. + SelectBeatmap(sender[changes.NewModifiedIndices.First()].Beatmaps.First()); } } } From e6a3659581e62b9c181d1c0ffe4f82a8988a8d05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jul 2022 14:23:47 +0900 Subject: [PATCH 03/52] Guard against `NewModifiedIndices` being empty --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 0430c15a1d..e9419e7156 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -273,7 +273,7 @@ namespace osu.Game.Screens.Select // Check if the current selection was potentially deleted by re-querying its validity. bool selectedSetMarkedDeleted = realm.Run(r => r.Find(SelectedBeatmapSet.ID))?.DeletePending != false; - if (selectedSetMarkedDeleted) + if (selectedSetMarkedDeleted && changes.NewModifiedIndices.Any()) { // If it is no longer valid, make the bold assumption that an updated version will be available in the modified indices. // This relies on the full update operation being in a single transaction, so please don't change that. From 037f56077bdbcc993e46f23fd682e43fc2f86941 Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 12 Aug 2022 13:29:04 +1000 Subject: [PATCH 04/52] Apply Flashlight grid nerf --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 12 ++++++++++++ .../Difficulty/Skills/Flashlight.cs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index fcf4179a3b..f1ae68ec73 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -18,11 +18,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const double min_velocity = 0.5; private const double slider_multiplier = 1.3; + private const double min_grid_multiplier = 0.35; + /// /// Evaluates the difficulty of memorising and hitting an object, based on: /// /// distance between a number of previous objects and the current object, /// the visual opacity of the current object, + /// the angle made by the current object, /// length and speed of the current object (for sliders), /// and whether the hidden mod is enabled. /// @@ -77,6 +80,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (hidden) result *= 1.0 + hidden_bonus; + // Nerf patterns with angles that are commonly used in grid maps. + // 0 deg, 60 deg, 120 deg and 180 deg are commonly used in hexgrid maps. + // 0 deg, 45 deg, 90 deg, 135 deg and 180 deg are commonly used in squaregrid maps. + if (osuCurrent.Angle != null) { + double hexgrid_multiplier = 1.0 - Math.Pow(Math.Cos((180 / 60.0) * (double)(osuCurrent.Angle)), 20.0); + double squaregrid_multiplier = 1.0 - Math.Pow(Math.Cos((180 / 45.0) * (double)(osuCurrent.Angle)), 20.0); + result *= (1.0 - min_grid_multiplier) * hexgrid_multiplier * squaregrid_multiplier + min_grid_multiplier; + } + double sliderBonus = 0.0; if (osuCurrent.BaseObject is Slider osuSlider) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 84ef109598..03130031ea 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills hasHiddenMod = mods.Any(m => m is OsuModHidden); } - private double skillMultiplier => 0.05; + private double skillMultiplier => 0.06; private double strainDecayBase => 0.15; private double currentStrain; From f70588a423b50c20030ab621ad6dd2d549ad935b Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 12 Aug 2022 14:08:32 +1000 Subject: [PATCH 05/52] Add newline before brace --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index f1ae68ec73..29434dcf49 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -83,7 +83,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Nerf patterns with angles that are commonly used in grid maps. // 0 deg, 60 deg, 120 deg and 180 deg are commonly used in hexgrid maps. // 0 deg, 45 deg, 90 deg, 135 deg and 180 deg are commonly used in squaregrid maps. - if (osuCurrent.Angle != null) { + if (osuCurrent.Angle != null) + { double hexgrid_multiplier = 1.0 - Math.Pow(Math.Cos((180 / 60.0) * (double)(osuCurrent.Angle)), 20.0); double squaregrid_multiplier = 1.0 - Math.Pow(Math.Cos((180 / 45.0) * (double)(osuCurrent.Angle)), 20.0); result *= (1.0 - min_grid_multiplier) * hexgrid_multiplier * squaregrid_multiplier + min_grid_multiplier; From 21c5fed45f48830d24ec448840b22f4b38e7504e Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 12 Aug 2022 14:09:16 +1000 Subject: [PATCH 06/52] Adjust capitalisation --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 29434dcf49..9d2696c978 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -85,9 +85,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // 0 deg, 45 deg, 90 deg, 135 deg and 180 deg are commonly used in squaregrid maps. if (osuCurrent.Angle != null) { - double hexgrid_multiplier = 1.0 - Math.Pow(Math.Cos((180 / 60.0) * (double)(osuCurrent.Angle)), 20.0); - double squaregrid_multiplier = 1.0 - Math.Pow(Math.Cos((180 / 45.0) * (double)(osuCurrent.Angle)), 20.0); - result *= (1.0 - min_grid_multiplier) * hexgrid_multiplier * squaregrid_multiplier + min_grid_multiplier; + double hexgridMultiplier = 1.0 - Math.Pow(Math.Cos((180 / 60.0) * (double)(osuCurrent.Angle)), 20.0); + double squaregridMultiplier = 1.0 - Math.Pow(Math.Cos((180 / 45.0) * (double)(osuCurrent.Angle)), 20.0); + result *= (1.0 - min_grid_multiplier) * hexgridMultiplier * squaregridMultiplier + min_grid_multiplier; } double sliderBonus = 0.0; From 9d2c2b71cf07a172a0f83e246e79e6b06822e29a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 16:21:35 +0900 Subject: [PATCH 07/52] Change conditional to check for insertions in addition to modifications It is possible that the import process itself marks the previous beatmaps as deleted due to an overlap in metadata or otherwise. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index e9419e7156..e9f676d32f 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -273,11 +273,13 @@ namespace osu.Game.Screens.Select // Check if the current selection was potentially deleted by re-querying its validity. bool selectedSetMarkedDeleted = realm.Run(r => r.Find(SelectedBeatmapSet.ID))?.DeletePending != false; - if (selectedSetMarkedDeleted && changes.NewModifiedIndices.Any()) + int[] modifiedAndInserted = changes.NewModifiedIndices.Concat(changes.InsertedIndices).ToArray(); + + if (selectedSetMarkedDeleted && modifiedAndInserted.Any()) { // If it is no longer valid, make the bold assumption that an updated version will be available in the modified indices. // This relies on the full update operation being in a single transaction, so please don't change that. - foreach (int i in changes.NewModifiedIndices) + foreach (int i in modifiedAndInserted) { var beatmapSetInfo = sender[i]; From f5710d8000d3bc11c4a2065f411105c53e249914 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 16:10:55 +0900 Subject: [PATCH 08/52] Add ruleset API versioning --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 ++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 ++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 ++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 ++ osu.Game/Rulesets/Ruleset.cs | 17 +++++++++++++++++ 5 files changed, 25 insertions(+) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index ed151855b1..f94bf276a0 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Catch public const string SHORT_NAME = "fruits"; + public override string RulesetAPIVersionSupported => "internal"; + public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { new KeyBinding(InputKey.Z, CatchAction.MoveLeft), diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index ac6060ceed..b1fe4b30c4 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -60,6 +60,8 @@ namespace osu.Game.Rulesets.Mania public const string SHORT_NAME = "mania"; + public override string RulesetAPIVersionSupported => "internal"; + public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this); public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new ManiaLegacySkinTransformer(skin, beatmap); diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 7f58f29d4b..4400dfbb65 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -54,6 +54,8 @@ namespace osu.Game.Rulesets.Osu public const string SHORT_NAME = "osu"; + public override string RulesetAPIVersionSupported => "internal"; + public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { new KeyBinding(InputKey.Z, OsuAction.LeftButton), diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 04bb08395b..275c7144a7 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -49,6 +49,8 @@ namespace osu.Game.Rulesets.Taiko public const string SHORT_NAME = "taiko"; + public override string RulesetAPIVersionSupported => "internal"; + public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre), diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 0968d78ed7..63f5906f46 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -44,6 +44,23 @@ namespace osu.Game.Rulesets private static readonly ConcurrentDictionary mod_reference_cache = new ConcurrentDictionary(); + /// + /// Version history: + /// 2022.205.0 FramedReplayInputHandler.CollectPendingInputs renamed to FramedReplayHandler.CollectReplayInputs. + /// 2022.822.0 All strings return values have been converted to LocalisableString to allow for localisation support. + /// + public const string CURRENT_RULESET_API_VERSION = "2022.822.0"; + + /// + /// Define the ruleset API version supported by this ruleset. + /// Ruleset implementations should be updated to support the latest version to ensure they can still be loaded. + /// + /// + /// When updating a ruleset to support the latest API, you should set this to . + /// See https://github.com/ppy/osu/wiki/Breaking-Changes for full details on required ongoing changes. + /// + public virtual string RulesetAPIVersionSupported => string.Empty; + /// /// A queryable source containing all available mods. /// Call for consumption purposes. From 758a554180fb61f21dbc8b9eae6d90d674358ded Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 16:31:25 +0900 Subject: [PATCH 09/52] Add basic check for correct ruleset API version --- osu.Game/Rulesets/RealmRulesetStore.cs | 28 +++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/RealmRulesetStore.cs b/osu.Game/Rulesets/RealmRulesetStore.cs index dba7f47f2f..590f118b68 100644 --- a/osu.Game/Rulesets/RealmRulesetStore.cs +++ b/osu.Game/Rulesets/RealmRulesetStore.cs @@ -77,9 +77,16 @@ namespace osu.Game.Rulesets continue; } - var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo + var instance = (Activator.CreateInstance(resolvedType) as Ruleset); + var instanceInfo = instance?.RulesetInfo ?? throw new RulesetLoadException(@"Instantiation failure"); + if (!checkRulesetUpToDate(instance)) + { + throw new ArgumentOutOfRangeException(nameof(instance.RulesetAPIVersionSupported), + $"Ruleset API version is too old (was {instance.RulesetAPIVersionSupported}, expected {Ruleset.CURRENT_RULESET_API_VERSION})"); + } + // If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution. // To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw. resolvedType.Assembly.GetTypes(); @@ -104,6 +111,25 @@ namespace osu.Game.Rulesets }); } + private bool checkRulesetUpToDate(Ruleset instance) + { + switch (instance.RulesetAPIVersionSupported) + { + // The default `virtual` implementation leaves the version string empty. + // Consider rulesets which haven't override the version as up-to-date for now. + // At some point (once ruleset devs add versioning), we'll probably want to disallow this for deployed builds. + case @"": + // Rulesets which are bundled with the game. Saves having to update their versions each bump. + case @"internal": + // Ruleset is up-to-date, all good. + case Ruleset.CURRENT_RULESET_API_VERSION: + return true; + + default: + return false; + } + } + private void testRulesetCompatibility(RulesetInfo rulesetInfo) { // do various operations to ensure that we are in a good state. From b0e7f63361aa072841a0614b6454aeb5aa243158 Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 26 Aug 2022 12:34:33 +1000 Subject: [PATCH 10/52] Update angle multiplier to nerf repeated angles --- .../Evaluators/FlashlightEvaluator.cs | 28 ++++++++++++------- .../Difficulty/Skills/Flashlight.cs | 2 +- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 9d2696c978..8cf8190554 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -7,6 +7,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; +using osu.Framework.Utils; namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const double min_velocity = 0.5; private const double slider_multiplier = 1.3; - private const double min_grid_multiplier = 0.35; + private const double min_angle_multiplier = 0.2; /// /// Evaluates the difficulty of memorising and hitting an object, based on: @@ -46,6 +47,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators OsuDifficultyHitObject lastObj = osuCurrent; + int angleRepeatCount = 0; + + // We want to round angles to make abusing the nerf a bit harder. + double initialRoundedAngle = 0.0; + if (osuCurrent.Angle != null) + initialRoundedAngle = Math.Round(MathUtils.RadiansToDegrees(osuCurrent.Angle.Value) / 2.0) * 2.0; + // This is iterating backwards in time from the current object. for (int i = 0; i < Math.Min(current.Index, 10); i++) { @@ -69,6 +77,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - osuCurrent.OpacityAt(currentHitObject.StartTime, hidden)); result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime; + + if (currentObj.Angle != null && osuCurrent.Angle != null) { + double roundedAngle = Math.Round(MathUtils.RadiansToDegrees(currentObj.Angle.Value) / 2.0) * 2.0; + + if (roundedAngle == initialRoundedAngle) + angleRepeatCount++; + } } lastObj = currentObj; @@ -80,15 +95,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (hidden) result *= 1.0 + hidden_bonus; - // Nerf patterns with angles that are commonly used in grid maps. - // 0 deg, 60 deg, 120 deg and 180 deg are commonly used in hexgrid maps. - // 0 deg, 45 deg, 90 deg, 135 deg and 180 deg are commonly used in squaregrid maps. - if (osuCurrent.Angle != null) - { - double hexgridMultiplier = 1.0 - Math.Pow(Math.Cos((180 / 60.0) * (double)(osuCurrent.Angle)), 20.0); - double squaregridMultiplier = 1.0 - Math.Pow(Math.Cos((180 / 45.0) * (double)(osuCurrent.Angle)), 20.0); - result *= (1.0 - min_grid_multiplier) * hexgridMultiplier * squaregridMultiplier + min_grid_multiplier; - } + // Nerf patterns with repeated angles. + result *= min_angle_multiplier + (1.0 - min_angle_multiplier) / (angleRepeatCount + 1.0); double sliderBonus = 0.0; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 03130031ea..84ef109598 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills hasHiddenMod = mods.Any(m => m is OsuModHidden); } - private double skillMultiplier => 0.06; + private double skillMultiplier => 0.05; private double strainDecayBase => 0.15; private double currentStrain; From 6651e76e2e9215ebfe700d5fcb4720c2ae265308 Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 26 Aug 2022 12:37:56 +1000 Subject: [PATCH 11/52] Remove whitespace --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 8cf8190554..f3953bdb79 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -80,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (currentObj.Angle != null && osuCurrent.Angle != null) { double roundedAngle = Math.Round(MathUtils.RadiansToDegrees(currentObj.Angle.Value) / 2.0) * 2.0; - + if (roundedAngle == initialRoundedAngle) angleRepeatCount++; } From d8854413cbbcf0607edd6093c9b0cfea8935a3ed Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 26 Aug 2022 12:38:36 +1000 Subject: [PATCH 12/52] Add newline --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index f3953bdb79..86b6170d13 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -78,7 +78,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime; - if (currentObj.Angle != null && osuCurrent.Angle != null) { + if (currentObj.Angle != null && osuCurrent.Angle != null) + { double roundedAngle = Math.Round(MathUtils.RadiansToDegrees(currentObj.Angle.Value) / 2.0) * 2.0; if (roundedAngle == initialRoundedAngle) From 31e459364bd2eb6a5c6879447fbd596dda0f5b52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 14:11:42 +0900 Subject: [PATCH 13/52] Use `FramedBeatmapClock` in `EditorClock` --- osu.Game/Screens/Edit/EditorClock.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index c3b29afd30..d55f2c0bf6 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -4,10 +4,12 @@ #nullable disable using System; +using System.Diagnostics; using System.Linq; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Transforms; using osu.Framework.Timing; using osu.Framework.Utils; @@ -19,7 +21,7 @@ namespace osu.Game.Screens.Edit /// /// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor. /// - public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock + public class EditorClock : CompositeComponent, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock { public IBindable Track => track; @@ -33,7 +35,7 @@ namespace osu.Game.Screens.Edit private readonly BindableBeatDivisor beatDivisor; - private readonly DecoupleableInterpolatingFramedClock underlyingClock; + private readonly FramedBeatmapClock underlyingClock; private bool playbackFinished; @@ -52,7 +54,8 @@ namespace osu.Game.Screens.Edit this.beatDivisor = beatDivisor ?? new BindableBeatDivisor(); - underlyingClock = new DecoupleableInterpolatingFramedClock(); + underlyingClock = new FramedBeatmapClock(applyOffsets: true); + AddInternal(underlyingClock); } /// @@ -219,6 +222,9 @@ namespace osu.Game.Screens.Edit public void ProcessFrame() { + // EditorClock wasn't being added in many places. This gives us more certainty that it is. + Debug.Assert(underlyingClock.LoadState > LoadState.NotLoaded); + underlyingClock.ProcessFrame(); playbackFinished = CurrentTime >= TrackLength; From fec744a7fe5b4d44fdcd801ef63ad14d63ee334f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 17:44:33 +0900 Subject: [PATCH 14/52] Add global `FramedBeatmapClock` for `BeatSyncProvider` components --- osu.Game/OsuGameBase.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8d5c58d5f0..27ea3a76ae 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -185,6 +185,12 @@ namespace osu.Game private RealmAccess realm; + /// + /// For now, this is used as a source specifically for beat synced components. + /// Going forward, it could potentially be used as the single source-of-truth for beatmap timing. + /// + private readonly FramedBeatmapClock beatmapClock = new FramedBeatmapClock(true); + protected override Container Content => content; private Container content; @@ -368,10 +374,15 @@ namespace osu.Game AddInternal(MusicController = new MusicController()); dependencies.CacheAs(MusicController); + MusicController.TrackChanged += onTrackChanged; + AddInternal(beatmapClock); + Ruleset.BindValueChanged(onRulesetChanged); Beatmap.BindValueChanged(onBeatmapChanged); } + private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) => beatmapClock.ChangeSource(beatmap.Track); + protected virtual void InitialiseFonts() { AddFont(Resources, @"Fonts/osuFont"); @@ -587,7 +598,7 @@ namespace osu.Game } ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.BeatmapLoaded ? Beatmap.Value.Beatmap.ControlPointInfo : null; - IClock IBeatSyncProvider.Clock => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track : (IClock)null; + IClock IBeatSyncProvider.Clock => beatmapClock; ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : ChannelAmplitudes.Empty; } } From 12d6d6793cd5eba002c0b39a976dc15c8c8955b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 18:08:39 +0900 Subject: [PATCH 15/52] Move `EditorClock` processing to `Update` and always decouple --- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/Edit/EditorClock.cs | 38 ++++++++++++---------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a7cbe1f1ad..ad8fda7ad0 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -220,7 +220,7 @@ namespace osu.Game.Screens.Edit } // Todo: should probably be done at a DrawableRuleset level to share logic with Player. - clock = new EditorClock(playableBeatmap, beatDivisor) { IsCoupled = false }; + clock = new EditorClock(playableBeatmap, beatDivisor); clock.ChangeSource(loadableBeatmap.Track); dependencies.CacheAs(clock); diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index d55f2c0bf6..7a00a74530 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Edit this.beatDivisor = beatDivisor ?? new BindableBeatDivisor(); - underlyingClock = new FramedBeatmapClock(applyOffsets: true); + underlyingClock = new FramedBeatmapClock(applyOffsets: true) { IsCoupled = false }; AddInternal(underlyingClock); } @@ -222,21 +222,7 @@ namespace osu.Game.Screens.Edit public void ProcessFrame() { - // EditorClock wasn't being added in many places. This gives us more certainty that it is. - Debug.Assert(underlyingClock.LoadState > LoadState.NotLoaded); - - underlyingClock.ProcessFrame(); - - playbackFinished = CurrentTime >= TrackLength; - - if (playbackFinished) - { - if (IsRunning) - underlyingClock.Stop(); - - if (CurrentTime > TrackLength) - underlyingClock.Seek(TrackLength); - } + // Noop to ensure an external consumer doesn't process the internal clock an extra time. } public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime; @@ -253,18 +239,26 @@ namespace osu.Game.Screens.Edit public IClock Source => underlyingClock.Source; - public bool IsCoupled - { - get => underlyingClock.IsCoupled; - set => underlyingClock.IsCoupled = value; - } - private const double transform_time = 300; protected override void Update() { base.Update(); + // EditorClock wasn't being added in many places. This gives us more certainty that it is. + Debug.Assert(underlyingClock.LoadState > LoadState.NotLoaded); + + playbackFinished = CurrentTime >= TrackLength; + + if (playbackFinished) + { + if (IsRunning) + underlyingClock.Stop(); + + if (CurrentTime > TrackLength) + underlyingClock.Seek(TrackLength); + } + updateSeekingState(); } From 4b72e557701bf8358d771a710950d9126d593b93 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 15:40:01 +0900 Subject: [PATCH 16/52] Fix various test scenes not adding `EditorClock` to the draw hierarchy --- .../CatchSelectionBlueprintTestScene.cs | 9 +- .../Editor/TestSceneManiaHitObjectComposer.cs | 8 +- .../Editor/TestSceneOsuDistanceSnapGrid.cs | 3 +- .../Editor/TestSceneTaikoHitObjectComposer.cs | 2 +- ...tSceneHitObjectComposerDistanceSnapping.cs | 18 +- .../Visual/Editing/TestSceneEditorClock.cs | 48 +++--- .../Editing/TestSceneEditorSeekSnapping.cs | 159 +++++++++--------- .../Editing/TestSceneTapTimingControl.cs | 4 +- .../TestSceneTimelineBlueprintContainer.cs | 2 +- .../Visual/Editing/TestSceneTimingScreen.cs | 12 +- .../Visual/Editing/TimelineTestScene.cs | 2 +- osu.Game/Tests/Visual/EditorClockTestScene.cs | 44 ++--- .../Visual/PlacementBlueprintTestScene.cs | 11 +- .../Visual/SelectionBlueprintTestScene.cs | 17 +- 14 files changed, 174 insertions(+), 165 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs index ea17fa400c..60fb31d1e0 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs @@ -70,10 +70,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor [Cached] private readonly BindableBeatDivisor beatDivisor; + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + public EditorBeatmapDependencyContainer(IBeatmap beatmap, BindableBeatDivisor beatDivisor) { - editorClock = new EditorClock(beatmap, beatDivisor); this.beatDivisor = beatDivisor; + + InternalChildren = new Drawable[] + { + editorClock = new EditorClock(beatmap, beatDivisor), + Content, + }; } } } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index 4ad27348fc..fcc9e2e6c3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor public void Setup() => Schedule(() => { BeatDivisor.Value = 8; - Clock.Seek(0); + EditorClock.Seek(0); Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both }; }); @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); originalTime = lastObject.HitObject.StartTime; - Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); + EditorClock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); }); AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects)); @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); originalTime = lastObject.HitObject.StartTime; - Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); + EditorClock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); }); AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects)); @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor AddStep("seek to last object", () => { lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); - Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); + EditorClock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); }); AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects)); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index e54ce45ccc..c102678e00 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -59,10 +59,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } }); - editorClock = new EditorClock(editorBeatmap); - base.Content.Children = new Drawable[] { + editorClock = new EditorClock(editorBeatmap), snapProvider, Content }; diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs index 16f17f4131..8d17918a92 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor public void Setup() => Schedule(() => { BeatDivisor.Value = 8; - Clock.Seek(0); + EditorClock.Seek(0); Child = new TestComposer { RelativeSizeAxes = Axes.Both }; }); diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 0e80f8f699..1e87ed27df 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -23,13 +21,13 @@ namespace osu.Game.Tests.Editing [HeadlessTest] public class TestSceneHitObjectComposerDistanceSnapping : EditorClockTestScene { - private TestHitObjectComposer composer; + private TestHitObjectComposer composer = null!; [Cached(typeof(EditorBeatmap))] [Cached(typeof(IBeatSnapProvider))] private readonly EditorBeatmap editorBeatmap; - protected override Container Content { get; } + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; public TestSceneHitObjectComposerDistanceSnapping() { @@ -40,15 +38,9 @@ namespace osu.Game.Tests.Editing { editorBeatmap = new EditorBeatmap(new OsuBeatmap { - BeatmapInfo = - { - Ruleset = new OsuRuleset().RulesetInfo, - }, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, }), - Content = new Container - { - RelativeSizeAxes = Axes.Both, - } + Content }, }); } @@ -205,7 +197,7 @@ namespace osu.Game.Tests.Editing assertSnappedDistance(400, 400); } - private void assertSnapDistance(float expectedDistance, HitObject hitObject = null) + private void assertSnapDistance(float expectedDistance, HitObject? hitObject = null) => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()), () => Is.EqualTo(expectedDistance)); private void assertDurationToDistance(double duration, float expectedDistance) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs index 3c6820e49b..d598ebafa9 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs @@ -55,51 +55,51 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestStopAtTrackEnd() { - AddStep("reset clock", () => Clock.Seek(0)); + AddStep("reset clock", () => EditorClock.Seek(0)); - AddStep("start clock", () => Clock.Start()); - AddAssert("clock running", () => Clock.IsRunning); + AddStep("start clock", () => EditorClock.Start()); + AddAssert("clock running", () => EditorClock.IsRunning); - AddStep("seek near end", () => Clock.Seek(Clock.TrackLength - 250)); - AddUntilStep("clock stops", () => !Clock.IsRunning); + AddStep("seek near end", () => EditorClock.Seek(EditorClock.TrackLength - 250)); + AddUntilStep("clock stops", () => !EditorClock.IsRunning); - AddUntilStep("clock stopped at end", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); + AddUntilStep("clock stopped at end", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength)); - AddStep("start clock again", () => Clock.Start()); - AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); + AddStep("start clock again", () => EditorClock.Start()); + AddAssert("clock looped to start", () => EditorClock.IsRunning && EditorClock.CurrentTime < 500); } [Test] public void TestWrapWhenStoppedAtTrackEnd() { - AddStep("reset clock", () => Clock.Seek(0)); + AddStep("reset clock", () => EditorClock.Seek(0)); - AddStep("stop clock", () => Clock.Stop()); - AddAssert("clock stopped", () => !Clock.IsRunning); + AddStep("stop clock", () => EditorClock.Stop()); + AddAssert("clock stopped", () => !EditorClock.IsRunning); - AddStep("seek exactly to end", () => Clock.Seek(Clock.TrackLength)); - AddAssert("clock stopped at end", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); + AddStep("seek exactly to end", () => EditorClock.Seek(EditorClock.TrackLength)); + AddAssert("clock stopped at end", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength)); - AddStep("start clock again", () => Clock.Start()); - AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); + AddStep("start clock again", () => EditorClock.Start()); + AddAssert("clock looped to start", () => EditorClock.IsRunning && EditorClock.CurrentTime < 500); } [Test] public void TestClampWhenSeekOutsideBeatmapBounds() { - AddStep("stop clock", () => Clock.Stop()); + AddStep("stop clock", () => EditorClock.Stop()); - AddStep("seek before start time", () => Clock.Seek(-1000)); - AddAssert("time is clamped to 0", () => Clock.CurrentTime, () => Is.EqualTo(0)); + AddStep("seek before start time", () => EditorClock.Seek(-1000)); + AddAssert("time is clamped to 0", () => EditorClock.CurrentTime, () => Is.EqualTo(0)); - AddStep("seek beyond track length", () => Clock.Seek(Clock.TrackLength + 1000)); - AddAssert("time is clamped to track length", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); + AddStep("seek beyond track length", () => EditorClock.Seek(EditorClock.TrackLength + 1000)); + AddAssert("time is clamped to track length", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength)); - AddStep("seek smoothly before start time", () => Clock.SeekSmoothlyTo(-1000)); - AddUntilStep("time is clamped to 0", () => Clock.CurrentTime, () => Is.EqualTo(0)); + AddStep("seek smoothly before start time", () => EditorClock.SeekSmoothlyTo(-1000)); + AddUntilStep("time is clamped to 0", () => EditorClock.CurrentTime, () => Is.EqualTo(0)); - AddStep("seek smoothly beyond track length", () => Clock.SeekSmoothlyTo(Clock.TrackLength + 1000)); - AddUntilStep("time is clamped to track length", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); + AddStep("seek smoothly beyond track length", () => EditorClock.SeekSmoothlyTo(EditorClock.TrackLength + 1000)); + AddUntilStep("time is clamped to track length", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength)); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs index 2465512dae..aa4bccd728 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs @@ -28,6 +28,11 @@ namespace osu.Game.Tests.Visual.Editing { base.LoadComplete(); + Child = new TimingPointVisualiser(Beatmap.Value.Beatmap, 5000) { Clock = EditorClock }; + } + + protected override Beatmap CreateEditorClockBeatmap() + { var testBeatmap = new Beatmap { ControlPointInfo = new ControlPointInfo(), @@ -45,9 +50,7 @@ namespace osu.Game.Tests.Visual.Editing testBeatmap.ControlPointInfo.Add(450, new TimingControlPoint { BeatLength = 100 }); testBeatmap.ControlPointInfo.Add(500, new TimingControlPoint { BeatLength = 307.69230769230802 }); - Beatmap.Value = CreateWorkingBeatmap(testBeatmap); - - Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock }; + return testBeatmap; } /// @@ -59,17 +62,17 @@ namespace osu.Game.Tests.Visual.Editing reset(); // Forwards - AddStep("Seek(0)", () => Clock.Seek(0)); + AddStep("Seek(0)", () => EditorClock.Seek(0)); checkTime(0); - AddStep("Seek(33)", () => Clock.Seek(33)); + AddStep("Seek(33)", () => EditorClock.Seek(33)); checkTime(33); - AddStep("Seek(89)", () => Clock.Seek(89)); + AddStep("Seek(89)", () => EditorClock.Seek(89)); checkTime(89); // Backwards - AddStep("Seek(25)", () => Clock.Seek(25)); + AddStep("Seek(25)", () => EditorClock.Seek(25)); checkTime(25); - AddStep("Seek(0)", () => Clock.Seek(0)); + AddStep("Seek(0)", () => EditorClock.Seek(0)); checkTime(0); } @@ -82,19 +85,19 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("Seek(0), Snap", () => Clock.SeekSnapped(0)); + AddStep("Seek(0), Snap", () => EditorClock.SeekSnapped(0)); checkTime(0); - AddStep("Seek(50), Snap", () => Clock.SeekSnapped(50)); + AddStep("Seek(50), Snap", () => EditorClock.SeekSnapped(50)); checkTime(50); - AddStep("Seek(100), Snap", () => Clock.SeekSnapped(100)); + AddStep("Seek(100), Snap", () => EditorClock.SeekSnapped(100)); checkTime(100); - AddStep("Seek(175), Snap", () => Clock.SeekSnapped(175)); + AddStep("Seek(175), Snap", () => EditorClock.SeekSnapped(175)); checkTime(175); - AddStep("Seek(350), Snap", () => Clock.SeekSnapped(350)); + AddStep("Seek(350), Snap", () => EditorClock.SeekSnapped(350)); checkTime(350); - AddStep("Seek(400), Snap", () => Clock.SeekSnapped(400)); + AddStep("Seek(400), Snap", () => EditorClock.SeekSnapped(400)); checkTime(400); - AddStep("Seek(450), Snap", () => Clock.SeekSnapped(450)); + AddStep("Seek(450), Snap", () => EditorClock.SeekSnapped(450)); checkTime(450); } @@ -107,17 +110,17 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("Seek(24), Snap", () => Clock.SeekSnapped(24)); + AddStep("Seek(24), Snap", () => EditorClock.SeekSnapped(24)); checkTime(0); - AddStep("Seek(26), Snap", () => Clock.SeekSnapped(26)); + AddStep("Seek(26), Snap", () => EditorClock.SeekSnapped(26)); checkTime(50); - AddStep("Seek(150), Snap", () => Clock.SeekSnapped(150)); + AddStep("Seek(150), Snap", () => EditorClock.SeekSnapped(150)); checkTime(100); - AddStep("Seek(170), Snap", () => Clock.SeekSnapped(170)); + AddStep("Seek(170), Snap", () => EditorClock.SeekSnapped(170)); checkTime(175); - AddStep("Seek(274), Snap", () => Clock.SeekSnapped(274)); + AddStep("Seek(274), Snap", () => EditorClock.SeekSnapped(274)); checkTime(175); - AddStep("Seek(276), Snap", () => Clock.SeekSnapped(276)); + AddStep("Seek(276), Snap", () => EditorClock.SeekSnapped(276)); checkTime(350); } @@ -129,15 +132,15 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("SeekForward", () => Clock.SeekForward()); + AddStep("SeekForward", () => EditorClock.SeekForward()); checkTime(50); - AddStep("SeekForward", () => Clock.SeekForward()); + AddStep("SeekForward", () => EditorClock.SeekForward()); checkTime(100); - AddStep("SeekForward", () => Clock.SeekForward()); + AddStep("SeekForward", () => EditorClock.SeekForward()); checkTime(200); - AddStep("SeekForward", () => Clock.SeekForward()); + AddStep("SeekForward", () => EditorClock.SeekForward()); checkTime(400); - AddStep("SeekForward", () => Clock.SeekForward()); + AddStep("SeekForward", () => EditorClock.SeekForward()); checkTime(450); } @@ -149,17 +152,17 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(50); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(100); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(175); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(350); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(400); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(450); } @@ -172,30 +175,30 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("Seek(49)", () => Clock.Seek(49)); + AddStep("Seek(49)", () => EditorClock.Seek(49)); checkTime(49); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(50); - AddStep("Seek(49.999)", () => Clock.Seek(49.999)); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("Seek(49.999)", () => EditorClock.Seek(49.999)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(100); - AddStep("Seek(99)", () => Clock.Seek(99)); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("Seek(99)", () => EditorClock.Seek(99)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(100); - AddStep("Seek(99.999)", () => Clock.Seek(99.999)); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("Seek(99.999)", () => EditorClock.Seek(99.999)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(150); - AddStep("Seek(174)", () => Clock.Seek(174)); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("Seek(174)", () => EditorClock.Seek(174)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(175); - AddStep("Seek(349)", () => Clock.Seek(349)); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("Seek(349)", () => EditorClock.Seek(349)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(350); - AddStep("Seek(399)", () => Clock.Seek(399)); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("Seek(399)", () => EditorClock.Seek(399)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(400); - AddStep("Seek(449)", () => Clock.Seek(449)); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("Seek(449)", () => EditorClock.Seek(449)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(450); } @@ -207,17 +210,17 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("Seek(450)", () => Clock.Seek(450)); + AddStep("Seek(450)", () => EditorClock.Seek(450)); checkTime(450); - AddStep("SeekBackward", () => Clock.SeekBackward()); + AddStep("SeekBackward", () => EditorClock.SeekBackward()); checkTime(400); - AddStep("SeekBackward", () => Clock.SeekBackward()); + AddStep("SeekBackward", () => EditorClock.SeekBackward()); checkTime(350); - AddStep("SeekBackward", () => Clock.SeekBackward()); + AddStep("SeekBackward", () => EditorClock.SeekBackward()); checkTime(150); - AddStep("SeekBackward", () => Clock.SeekBackward()); + AddStep("SeekBackward", () => EditorClock.SeekBackward()); checkTime(50); - AddStep("SeekBackward", () => Clock.SeekBackward()); + AddStep("SeekBackward", () => EditorClock.SeekBackward()); checkTime(0); } @@ -229,19 +232,19 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("Seek(450)", () => Clock.Seek(450)); + AddStep("Seek(450)", () => EditorClock.Seek(450)); checkTime(450); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(400); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(350); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(175); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(100); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(50); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(0); } @@ -254,18 +257,18 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("Seek(451)", () => Clock.Seek(451)); + AddStep("Seek(451)", () => EditorClock.Seek(451)); checkTime(451); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(450); - AddStep("Seek(450.999)", () => Clock.Seek(450.999)); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("Seek(450.999)", () => EditorClock.Seek(450.999)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(450); - AddStep("Seek(401)", () => Clock.Seek(401)); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("Seek(401)", () => EditorClock.Seek(401)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(400); - AddStep("Seek(401.999)", () => Clock.Seek(401.999)); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("Seek(401.999)", () => EditorClock.Seek(401.999)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(400); } @@ -279,37 +282,37 @@ namespace osu.Game.Tests.Visual.Editing double lastTime = 0; - AddStep("Seek(0)", () => Clock.Seek(0)); + AddStep("Seek(0)", () => EditorClock.Seek(0)); checkTime(0); for (int i = 0; i < 9; i++) { AddStep("SeekForward, Snap", () => { - lastTime = Clock.CurrentTime; - Clock.SeekForward(true); + lastTime = EditorClock.CurrentTime; + EditorClock.SeekForward(true); }); - AddAssert("Time > lastTime", () => Clock.CurrentTime > lastTime); + AddAssert("Time > lastTime", () => EditorClock.CurrentTime > lastTime); } for (int i = 0; i < 9; i++) { AddStep("SeekBackward, Snap", () => { - lastTime = Clock.CurrentTime; - Clock.SeekBackward(true); + lastTime = EditorClock.CurrentTime; + EditorClock.SeekBackward(true); }); - AddAssert("Time < lastTime", () => Clock.CurrentTime < lastTime); + AddAssert("Time < lastTime", () => EditorClock.CurrentTime < lastTime); } checkTime(0); } - private void checkTime(double expectedTime) => AddAssert($"Current time is {expectedTime}", () => Clock.CurrentTime, () => Is.EqualTo(expectedTime)); + private void checkTime(double expectedTime) => AddUntilStep($"Current time is {expectedTime}", () => EditorClock.CurrentTime, () => Is.EqualTo(expectedTime)); private void reset() { - AddStep("Reset", () => Clock.Seek(0)); + AddStep("Reset", () => EditorClock.Seek(0)); } private class TimingPointVisualiser : CompositeDrawable diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs index 7d881bc259..10e1206b53 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs @@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Editing .TriggerClick(); }); - AddUntilStep("wait for track playing", () => Clock.IsRunning); + AddUntilStep("wait for track playing", () => EditorClock.IsRunning); AddStep("click reset button", () => { @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Editing .TriggerClick(); }); - AddUntilStep("wait for track stopped", () => !Clock.IsRunning); + AddUntilStep("wait for track stopped", () => !EditorClock.IsRunning); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs index d2984728b0..c098b683a6 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.Editing protected override void LoadComplete() { base.LoadComplete(); - Clock.Seek(10000); + EditorClock.Seek(10000); } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index e262bd756a..03c184c27d 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Editing [SetUpSteps] public void SetUpSteps() { - AddStep("Stop clock", () => Clock.Stop()); + AddStep("Stop clock", () => EditorClock.Stop()); AddUntilStep("wait for rows to load", () => Child.ChildrenOfType().Any()); } @@ -68,10 +68,10 @@ namespace osu.Game.Tests.Visual.Editing }); AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670); - AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670); + AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 54670); - AddStep("Seek to just before next point", () => Clock.Seek(69000)); - AddStep("Start clock", () => Clock.Start()); + AddStep("Seek to just before next point", () => EditorClock.Seek(69000)); + AddStep("Start clock", () => EditorClock.Start()); AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670); } @@ -86,9 +86,9 @@ namespace osu.Game.Tests.Visual.Editing }); AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670); - AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670); + AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 54670); - AddStep("Seek to later", () => Clock.Seek(80000)); + AddStep("Seek to later", () => EditorClock.Seek(80000)); AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670); } diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 437f06c47f..d830f8d488 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Editing { base.LoadComplete(); - Clock.Seek(2500); + EditorClock.Seek(2500); } public abstract Drawable CreateTestComponent(); diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index 91284ae499..8f1e7abd9e 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -6,6 +6,8 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Overlays; @@ -24,30 +26,39 @@ namespace osu.Game.Tests.Visual protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor(); - [Cached] - protected new readonly EditorClock Clock; + protected EditorClock EditorClock; private readonly Bindable frequencyAdjustment = new BindableDouble(1); + private IBeatmap editorClockBeatmap; protected virtual bool ScrollUsingMouseWheel => true; - protected EditorClockTestScene() - { - Clock = new EditorClock(new Beatmap(), BeatDivisor) { IsCoupled = false }; - } + protected override Container Content => content; + + private readonly Container content = new Container { RelativeSizeAxes = Axes.Both }; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + editorClockBeatmap = CreateEditorClockBeatmap(); + + base.Content.AddRange(new Drawable[] + { + EditorClock = new EditorClock(editorClockBeatmap, BeatDivisor), + content + }); + dependencies.Cache(BeatDivisor); - dependencies.CacheAs(Clock); + dependencies.CacheAs(EditorClock); return dependencies; } protected override void LoadComplete() { + Beatmap.Value = CreateWorkingBeatmap(editorClockBeatmap); + base.LoadComplete(); Beatmap.BindValueChanged(beatmapChanged, true); @@ -55,22 +66,13 @@ namespace osu.Game.Tests.Visual AddSliderStep("editor clock rate", 0.0, 2.0, 1.0, v => frequencyAdjustment.Value = v); } + protected virtual IBeatmap CreateEditorClockBeatmap() => new Beatmap(); + private void beatmapChanged(ValueChangedEvent e) { e.OldValue?.Track.RemoveAdjustment(AdjustableProperty.Frequency, frequencyAdjustment); - - Clock.Beatmap = e.NewValue.Beatmap; - Clock.ChangeSource(e.NewValue.Track); - Clock.ProcessFrame(); - e.NewValue.Track.AddAdjustment(AdjustableProperty.Frequency, frequencyAdjustment); - } - - protected override void Update() - { - base.Update(); - - Clock.ProcessFrame(); + EditorClock.ChangeSource(e.NewValue.Track); } protected override bool OnScroll(ScrollEvent e) @@ -79,9 +81,9 @@ namespace osu.Game.Tests.Visual return false; if (e.ScrollDelta.Y > 0) - Clock.SeekBackward(true); + EditorClock.SeekBackward(true); else - Clock.SeekForward(true); + EditorClock.SeekForward(true); return true; } diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index 797e8363c3..176b181e73 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -30,10 +30,15 @@ namespace osu.Game.Tests.Visual { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(new EditorClock()); - var playable = GetPlayableBeatmap(); - dependencies.CacheAs(new EditorBeatmap(playable)); + + var editorClock = new EditorClock(); + base.Content.Add(editorClock); + dependencies.CacheAs(editorClock); + + var editorBeatmap = new EditorBeatmap(playable); + // Not adding to hierarchy as we don't satisfy its dependencies. Probably not good. + dependencies.CacheAs(editorBeatmap); return dependencies; } diff --git a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs index 66d79cad1c..ac0d1cd366 100644 --- a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -19,19 +16,23 @@ namespace osu.Game.Tests.Visual [Cached] private readonly EditorClock editorClock = new EditorClock(); - protected override Container Content => content ?? base.Content; + protected override Container Content => content; private readonly Container content; protected SelectionBlueprintTestScene() { - base.Content.Add(content = new Container + base.Content.AddRange(new Drawable[] { - Clock = new FramedClock(new StopwatchClock()), - RelativeSizeAxes = Axes.Both + editorClock, + content = new Container + { + Clock = new FramedClock(new StopwatchClock()), + RelativeSizeAxes = Axes.Both + } }); } - protected void AddBlueprint(HitObjectSelectionBlueprint blueprint, [CanBeNull] DrawableHitObject drawableObject = null) + protected void AddBlueprint(HitObjectSelectionBlueprint blueprint, DrawableHitObject? drawableObject = null) { Add(blueprint.With(d => { From cd90536e4bbb881b1787390d4469a57a5e6299d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 18:25:48 +0900 Subject: [PATCH 17/52] Remove `Track` access in `Timeline` --- .../Compose/Components/Timeline/Timeline.cs | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 54f2d13707..9e96a7386d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -64,8 +63,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// private bool trackWasPlaying; - private Track track; - /// /// The timeline zoom level at a 1x zoom scale. /// @@ -93,6 +90,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Bindable waveformOpacity; + private double trackLengthForZoom; + [BackgroundDependencyLoader] private void load(IBindable beatmap, OsuColour colours, OsuConfigManager config) { @@ -144,9 +143,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Beatmap.BindValueChanged(b => { waveform.Waveform = b.NewValue.Waveform; - track = b.NewValue.Track; - - setupTimelineZoom(); }, true); Zoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom); @@ -185,8 +181,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateWaveformOpacity() => waveform.FadeTo(WaveformVisible.Value ? waveformOpacity.Value : 0, 200, Easing.OutQuint); - private float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(track.Length / milliseconds)); - protected override void Update() { base.Update(); @@ -197,20 +191,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // This needs to happen after transforms are updated, but before the scroll position is updated in base.UpdateAfterChildren if (editorClock.IsRunning) scrollToTrackTime(); - } - private void setupTimelineZoom() - { - if (!track.IsLoaded) + if (editorClock.TrackLength != trackLengthForZoom) { - Scheduler.AddOnce(setupTimelineZoom); - return; + defaultTimelineZoom = getZoomLevelForVisibleMilliseconds(6000); + + float initialZoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom); + float minimumZoom = getZoomLevelForVisibleMilliseconds(10000); + float maximumZoom = getZoomLevelForVisibleMilliseconds(500); + + SetupZoom(initialZoom, minimumZoom, maximumZoom); + + float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(editorClock.TrackLength / milliseconds)); + + trackLengthForZoom = editorClock.TrackLength; } - - defaultTimelineZoom = getZoomLevelForVisibleMilliseconds(6000); - - float initialZoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom); - SetupZoom(initialZoom, getZoomLevelForVisibleMilliseconds(10000), getZoomLevelForVisibleMilliseconds(500)); } protected override bool OnScroll(ScrollEvent e) @@ -255,16 +250,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void seekTrackToCurrent() { - if (!track.IsLoaded) - return; - - double target = Current / Content.DrawWidth * track.Length; - editorClock.Seek(Math.Min(track.Length, target)); + double target = Current / Content.DrawWidth * editorClock.TrackLength; + editorClock.Seek(Math.Min(editorClock.TrackLength, target)); } private void scrollToTrackTime() { - if (!track.IsLoaded || track.Length == 0) + if (editorClock.TrackLength == 0) return; // covers the case where the user starts playback after a drag is in progress. @@ -272,7 +264,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (handlingDragInput) editorClock.Stop(); - ScrollTo((float)(editorClock.CurrentTime / track.Length) * Content.DrawWidth, false); + ScrollTo((float)(editorClock.CurrentTime / editorClock.TrackLength) * Content.DrawWidth, false); } protected override bool OnMouseDown(MouseDownEvent e) @@ -310,12 +302,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// /// The total amount of time visible on the timeline. /// - public double VisibleRange => track.Length / Zoom; + public double VisibleRange => editorClock.TrackLength / Zoom; public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) => new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); private double getTimeFromPosition(Vector2 localPosition) => - (localPosition.X / Content.DrawWidth) * track.Length; + (localPosition.X / Content.DrawWidth) * editorClock.TrackLength; } } From 9c9238d6e8686017d522752c3044156906393674 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 18:38:52 +0900 Subject: [PATCH 18/52] Fix `TimelineTestScene`'s beatmap getting overwritten by `EditorClockTestScene` --- .../Visual/Editing/TestSceneTimelineZoom.cs | 4 ---- .../Visual/Editing/TimelineTestScene.cs | 20 ++++++++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs index 630d048867..11ac102814 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs @@ -17,8 +17,6 @@ namespace osu.Game.Tests.Visual.Editing { double initialVisibleRange = 0; - AddUntilStep("wait for load", () => MusicController.TrackLoaded); - AddStep("reset zoom", () => TimelineArea.Timeline.Zoom = 100); AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange); @@ -36,8 +34,6 @@ namespace osu.Game.Tests.Visual.Editing { double initialVisibleRange = 0; - AddUntilStep("wait for load", () => MusicController.TrackLoaded); - AddStep("reset timeline size", () => TimelineArea.Timeline.Width = 1); AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange); diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index d830f8d488..19e297a08d 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -9,12 +9,14 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; +using osu.Game.Storyboards; using osuTK; using osuTK.Graphics; @@ -28,10 +30,14 @@ namespace osu.Game.Tests.Visual.Editing protected EditorBeatmap EditorBeatmap { get; private set; } - [BackgroundDependencyLoader] - private void load(AudioManager audio) + [Resolved] + private AudioManager audio { get; set; } + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new WaveformTestBeatmap(audio); + + protected override void LoadComplete() { - Beatmap.Value = new WaveformTestBeatmap(audio); + base.LoadComplete(); var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); EditorBeatmap = new EditorBeatmap(playable); @@ -68,11 +74,11 @@ namespace osu.Game.Tests.Visual.Editing }); } - protected override void LoadComplete() + [SetUpSteps] + public void SetUpSteps() { - base.LoadComplete(); - - EditorClock.Seek(2500); + AddUntilStep("wait for track loaded", () => MusicController.TrackLoaded); + AddStep("seek forward", () => EditorClock.Seek(2500)); } public abstract Drawable CreateTestComponent(); From 08cb70b0931eac8edcfc3939c0341166dff54ea1 Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 26 Aug 2022 20:27:31 +1000 Subject: [PATCH 19/52] Lessen repeated angle nerf for objects further back in time --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 86b6170d13..a6e76db902 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators OsuDifficultyHitObject lastObj = osuCurrent; - int angleRepeatCount = 0; + double angleRepeatCount = 0.0; // We want to round angles to make abusing the nerf a bit harder. double initialRoundedAngle = 0.0; @@ -82,8 +82,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { double roundedAngle = Math.Round(MathUtils.RadiansToDegrees(currentObj.Angle.Value) / 2.0) * 2.0; + // Objects further back in time should count less for the nerf. if (roundedAngle == initialRoundedAngle) - angleRepeatCount++; + angleRepeatCount += 1.0 - 0.1 * i; } } From 5082ee26cfeb62b7ce8560f9253e92f8434f606b Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 26 Aug 2022 20:30:14 +1000 Subject: [PATCH 20/52] Ensure a negative value cannot be added to angleRepeatCount --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index a6e76db902..2733217b64 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Objects further back in time should count less for the nerf. if (roundedAngle == initialRoundedAngle) - angleRepeatCount += 1.0 - 0.1 * i; + angleRepeatCount += Math.Max(1.0 - 0.1 * i, 0.0); } } From 249c3f868f6c426985a3631d2ad4f3773bbffb7d Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 26 Aug 2022 20:40:18 +1000 Subject: [PATCH 21/52] Compare raw angle values instead of rounding angles --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 2733217b64..9630da5a01 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -49,11 +49,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double angleRepeatCount = 0.0; - // We want to round angles to make abusing the nerf a bit harder. - double initialRoundedAngle = 0.0; - if (osuCurrent.Angle != null) - initialRoundedAngle = Math.Round(MathUtils.RadiansToDegrees(osuCurrent.Angle.Value) / 2.0) * 2.0; - // This is iterating backwards in time from the current object. for (int i = 0; i < Math.Min(current.Index, 10); i++) { @@ -80,10 +75,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (currentObj.Angle != null && osuCurrent.Angle != null) { - double roundedAngle = Math.Round(MathUtils.RadiansToDegrees(currentObj.Angle.Value) / 2.0) * 2.0; - // Objects further back in time should count less for the nerf. - if (roundedAngle == initialRoundedAngle) + if (Math.Abs(currentObj.Angle.Value - osuCurrent.Angle.Value) < 0.02) angleRepeatCount += Math.Max(1.0 - 0.1 * i, 0.0); } } From 454d868dd598148f7ea141f83de2e9dbdb73a97c Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 26 Aug 2022 20:42:02 +1000 Subject: [PATCH 22/52] Remove unnecessary using call --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 9630da5a01..2ba856d014 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -7,7 +7,6 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; -using osu.Framework.Utils; namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { From 81c0a641b4f596ca4c9b0a7d9c19b97c389f2ee1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 26 Aug 2022 14:51:08 +0300 Subject: [PATCH 23/52] Fix selection fallback path not updated to check inserted indices --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index e9f676d32f..0cbc17c67a 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -299,7 +299,7 @@ namespace osu.Game.Screens.Select // If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed. // Let's attempt to follow set-level selection anyway. - SelectBeatmap(sender[changes.NewModifiedIndices.First()].Beatmaps.First()); + SelectBeatmap(sender[modifiedAndInserted.First()].Beatmaps.First()); } } } From a3e595a9aac215987a6cb5ee5cad82cfad615a5c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 26 Aug 2022 14:51:19 +0300 Subject: [PATCH 24/52] Update comment to include inserted indices --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 0cbc17c67a..0f130714f1 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -277,7 +277,7 @@ namespace osu.Game.Screens.Select if (selectedSetMarkedDeleted && modifiedAndInserted.Any()) { - // If it is no longer valid, make the bold assumption that an updated version will be available in the modified indices. + // If it is no longer valid, make the bold assumption that an updated version will be available in the modified/inserted indices. // This relies on the full update operation being in a single transaction, so please don't change that. foreach (int i in modifiedAndInserted) { From b082dc1fe47f30662d7f2f81a3ac5766371a1bf9 Mon Sep 17 00:00:00 2001 From: MBmasher Date: Sat, 27 Aug 2022 18:31:07 +1000 Subject: [PATCH 25/52] Slightly buff flashlight multiplier --- osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 84ef109598..40448c444c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills hasHiddenMod = mods.Any(m => m is OsuModHidden); } - private double skillMultiplier => 0.05; + private double skillMultiplier => 0.052; private double strainDecayBase => 0.15; private double currentStrain; From 0cc6a76c17f6f5e9c0b36a747bfa349369f4e82c Mon Sep 17 00:00:00 2001 From: its5Q Date: Sun, 28 Aug 2022 14:13:38 +1000 Subject: [PATCH 26/52] Fix crash with legacy import from incomplete installs --- osu.Game/Database/LegacyBeatmapImporter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Database/LegacyBeatmapImporter.cs b/osu.Game/Database/LegacyBeatmapImporter.cs index 6805cb36b8..0955461609 100644 --- a/osu.Game/Database/LegacyBeatmapImporter.cs +++ b/osu.Game/Database/LegacyBeatmapImporter.cs @@ -20,6 +20,10 @@ namespace osu.Game.Database protected override IEnumerable GetStableImportPaths(Storage storage) { + // make sure the directory exists + if (!storage.ExistsDirectory(string.Empty)) + yield break; + foreach (string directory in storage.GetDirectories(string.Empty)) { var directoryStorage = storage.GetStorageForDirectory(directory); From c0b13c7e1fa33da7681957dd62326632cba564e1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Aug 2022 19:39:12 +0900 Subject: [PATCH 27/52] Refactor ScoreProcessor ComputeScore() methods --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 4 +- .../PerformanceBreakdownCalculator.cs | 2 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 60 +------------------ osu.Game/Scoring/ScoreManager.cs | 2 +- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 2 +- 5 files changed, 8 insertions(+), 62 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 44ebdad2e4..7a45d1e7cf 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -307,7 +307,7 @@ namespace osu.Game.Tests.Rulesets.Scoring HitObjects = { new TestHitObject(result) } }); - Assert.That(scoreProcessor.ComputeFinalScore(ScoringMode.Standardised, new ScoreInfo + Assert.That(scoreProcessor.ComputeScore(ScoringMode.Standardised, new ScoreInfo { Ruleset = new TestRuleset().RulesetInfo, MaxCombo = result.AffectsCombo() ? 1 : 0, @@ -350,7 +350,7 @@ namespace osu.Game.Tests.Rulesets.Scoring } }; - double totalScore = new TestScoreProcessor().ComputeFinalScore(ScoringMode.Standardised, testScore); + double totalScore = new TestScoreProcessor().ComputeScore(ScoringMode.Standardised, testScore); Assert.That(totalScore, Is.EqualTo(750_000)); // 500K from accuracy (100%), and 250K from combo (50%). } #pragma warning restore CS0618 diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 4465f1a328..3fb12041d1 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Difficulty // calculate total score ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = perfectPlay.Mods; - perfectPlay.TotalScore = (long)scoreProcessor.ComputeFinalScore(ScoringMode.Standardised, perfectPlay); + perfectPlay.TotalScore = (long)scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay); // compute rank achieved // default to SS, then adjust the rank with mods diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 5b21caee84..f13e3e6de6 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Scoring /// /// The maximum of a basic (non-tick and non-bonus) hitobject. - /// Only populated via or . + /// Only populated via or . /// private HitResult? maxBasicResult; @@ -281,7 +281,7 @@ namespace osu.Game.Rulesets.Scoring /// The to compute the total score of. /// The total score in the given . [Pure] - public double ComputeFinalScore(ScoringMode mode, ScoreInfo scoreInfo) + public double ComputeScore(ScoringMode mode, ScoreInfo scoreInfo) { if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); @@ -291,60 +291,6 @@ namespace osu.Game.Rulesets.Scoring return ComputeScore(mode, current, maximum); } - /// - /// Computes the total score of a partially-completed . This should be used when it is unknown whether a score is complete. - /// - /// - /// Requires to have been called before use. - /// - /// The to represent the score as. - /// The to compute the total score of. - /// The total score in the given . - [Pure] - public double ComputePartialScore(ScoringMode mode, ScoreInfo scoreInfo) - { - if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) - throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); - - if (!beatmapApplied) - throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}."); - - ExtractScoringValues(scoreInfo, out var current, out _); - - return ComputeScore(mode, current, MaximumScoringValues); - } - - /// - /// Computes the total score of a given with a given custom max achievable combo. - /// - /// - /// This is useful for processing legacy scores in which the maximum achievable combo can be more accurately determined via external means (e.g. database values or difficulty calculation). - ///

Does not require to have been called before use.

- ///
- /// The to represent the score as. - /// The to compute the total score of. - /// The maximum achievable combo for the provided beatmap. - /// The total score in the given . - [Pure] - public double ComputeFinalLegacyScore(ScoringMode mode, ScoreInfo scoreInfo, int maxAchievableCombo) - { - if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) - throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); - - double accuracyRatio = scoreInfo.Accuracy; - double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1; - - ExtractScoringValues(scoreInfo, out var current, out var maximum); - - // For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score. - // To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score. - // Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together. - if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3 && maximum.BaseScore > 0) - accuracyRatio = current.BaseScore / maximum.BaseScore; - - return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects); - } - /// /// Computes the total score from scoring values. /// @@ -454,7 +400,7 @@ namespace osu.Game.Rulesets.Scoring score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result); // Populate total score after everything else. - score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score)); + score.TotalScore = (long)Math.Round(ComputeScore(ScoringMode.Standardised, score)); } /// diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 7367a1ef77..ecd37c761c 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -148,7 +148,7 @@ namespace osu.Game.Scoring var scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = score.Mods; - return (long)Math.Round(scoreProcessor.ComputeFinalLegacyScore(mode, score, beatmapMaxCombo.Value)); + return (long)Math.Round(scoreProcessor.ComputeScore(mode, score)); } /// diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 282c1db585..1c4d02bb11 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); - Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.ComputeFinalScore(ScoringMode.Standardised, Score.ScoreInfo)); + Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo)); } protected override void Dispose(bool isDisposing) From 90b9c02ac64ca410fd8440d8d30c4a607aaeffeb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 14:01:04 +0900 Subject: [PATCH 28/52] Remove `"internal"` identifier as unnecessary --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 30 +++++++++---------- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 20 ++++++------- osu.Game.Rulesets.Osu/OsuRuleset.cs | 40 ++++++++++++------------- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 32 ++++++++++---------- osu.Game/Rulesets/RealmRulesetStore.cs | 2 -- osu.Game/Rulesets/Ruleset.cs | 37 ++++++++++++----------- 6 files changed, 80 insertions(+), 81 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index f94bf276a0..321399c597 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -3,30 +3,30 @@ #nullable disable -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Rulesets.Catch.Mods; -using osu.Game.Rulesets.Catch.UI; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.UI; +using System; using System.Collections.Generic; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; -using osu.Game.Rulesets.Catch.Replays; -using osu.Game.Rulesets.Replays.Types; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; +using osu.Game.Graphics; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty; -using osu.Game.Rulesets.Catch.Scoring; -using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Scoring; -using System; -using osu.Framework.Extensions.EnumExtensions; -using osu.Framework.Localisation; using osu.Game.Rulesets.Catch.Edit; +using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Catch.Replays; +using osu.Game.Rulesets.Catch.Scoring; using osu.Game.Rulesets.Catch.Skinning.Legacy; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays.Types; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch public const string SHORT_NAME = "fruits"; - public override string RulesetAPIVersionSupported => "internal"; + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index b1fe4b30c4..813e2c461a 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -4,11 +4,6 @@ #nullable disable using System; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mania.Mods; -using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.UI; using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.EnumExtensions; @@ -16,11 +11,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Localisation; -using osu.Game.Graphics; -using osu.Game.Rulesets.Mania.Replays; -using osu.Game.Rulesets.Replays.Types; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; @@ -31,13 +25,19 @@ using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Difficulty; using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.Edit.Setup; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Skinning.Legacy; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; -using osu.Game.Skinning; +using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Ranking.Statistics; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania { @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania public const string SHORT_NAME = "mania"; - public override string RulesetAPIVersionSupported => "internal"; + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this); diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 4400dfbb65..226299d168 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -3,42 +3,42 @@ #nullable disable -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.UI; +using System; using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; -using osu.Game.Overlays.Settings; using osu.Framework.Input.Bindings; -using osu.Game.Rulesets.Osu.Edit; -using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Osu.Replays; -using osu.Game.Rulesets.Replays.Types; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Difficulty; -using osu.Game.Rulesets.Osu.Scoring; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Skinning; -using System; -using System.Linq; -using osu.Framework.Extensions.EnumExtensions; -using osu.Framework.Localisation; +using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit.Setup; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.Statistics; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Replays.Types; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Ranking.Statistics; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu { @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu public const string SHORT_NAME = "osu"; - public override string RulesetAPIVersionSupported => "internal"; + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 275c7144a7..f4eb1c68b3 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -3,33 +3,33 @@ #nullable disable -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Mods; -using osu.Game.Rulesets.Taiko.UI; -using osu.Game.Rulesets.UI; +using System; using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; -using osu.Game.Rulesets.Replays.Types; -using osu.Game.Rulesets.Taiko.Replays; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; +using osu.Game.Graphics; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Difficulty; -using osu.Game.Rulesets.Taiko.Scoring; -using osu.Game.Scoring; -using System; -using System.Linq; -using osu.Framework.Extensions.EnumExtensions; -using osu.Framework.Localisation; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Edit; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Rulesets.Taiko.Skinning.Legacy; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Taiko public const string SHORT_NAME = "taiko"; - public override string RulesetAPIVersionSupported => "internal"; + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { diff --git a/osu.Game/Rulesets/RealmRulesetStore.cs b/osu.Game/Rulesets/RealmRulesetStore.cs index 590f118b68..456f6e399b 100644 --- a/osu.Game/Rulesets/RealmRulesetStore.cs +++ b/osu.Game/Rulesets/RealmRulesetStore.cs @@ -119,8 +119,6 @@ namespace osu.Game.Rulesets // Consider rulesets which haven't override the version as up-to-date for now. // At some point (once ruleset devs add versioning), we'll probably want to disallow this for deployed builds. case @"": - // Rulesets which are bundled with the game. Saves having to update their versions each bump. - case @"internal": // Ruleset is up-to-date, all good. case Ruleset.CURRENT_RULESET_API_VERSION: return true; diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 63f5906f46..cb72a1f20f 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -7,33 +7,33 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.IO.Stores; -using osu.Game.Beatmaps; -using osu.Game.Overlays.Settings; -using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Replays.Types; -using osu.Game.Rulesets.UI; -using osu.Game.Beatmaps.Legacy; -using osu.Game.Configuration; -using osu.Game.Rulesets.Configuration; -using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Skinning; -using osu.Game.Users; -using JetBrains.Annotations; -using osu.Framework.Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Localisation; using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Legacy; +using osu.Game.Configuration; using osu.Game.Extensions; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Configuration; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Filter; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays.Types; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Ranking.Statistics; +using osu.Game.Skinning; +using osu.Game.Users; namespace osu.Game.Rulesets { @@ -56,7 +56,8 @@ namespace osu.Game.Rulesets /// Ruleset implementations should be updated to support the latest version to ensure they can still be loaded. /// /// - /// When updating a ruleset to support the latest API, you should set this to . + /// Generally, all ruleset implementations should point this directly to . + /// This will ensure that each time you compile a new release, it will pull in the most recent version. /// See https://github.com/ppy/osu/wiki/Breaking-Changes for full details on required ongoing changes. /// public virtual string RulesetAPIVersionSupported => string.Empty; From 5ff4e6a4fe657b6c2f420bc1df3eddb028401638 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 14:26:09 +0900 Subject: [PATCH 29/52] Add test coverage for outdated ruleset --- osu.Game.Tests/Database/RulesetStoreTests.cs | 87 +++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index 795e90f543..dedec6dc83 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -1,11 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Difficulty; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.UI; namespace osu.Game.Tests.Database { @@ -51,5 +59,80 @@ namespace osu.Game.Tests.Database Assert.IsFalse(rulesets.GetRuleset("mania")?.IsManaged); }); } + + [Test] + public void TestOutdatedRulesetNotAvailable() + { + RunTestWithRealm((realm, storage) => + { + OutdatedRuleset.Version = "2021.101.0"; + OutdatedRuleset.HasImplementations = true; + + var ruleset = new OutdatedRuleset(); + string rulesetShortName = ruleset.RulesetInfo.ShortName; + + realm.Write(r => r.Add(new RulesetInfo(rulesetShortName, ruleset.RulesetInfo.Name, ruleset.RulesetInfo.InstantiationInfo, ruleset.RulesetInfo.OnlineID) + { + Available = true, + })); + + Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + + // Availability is updated on construction of a RealmRulesetStore + var _ = new RealmRulesetStore(realm, storage); + + Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.False); + + // Simulate the ruleset getting updated + OutdatedRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; + var __ = new RealmRulesetStore(realm, storage); + + Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + }); + } + + private class OutdatedRuleset : Ruleset + { + public override string RulesetAPIVersionSupported => Version; + + public static bool HasImplementations = true; + + public static string Version { get; set; } = CURRENT_RULESET_API_VERSION; + + public override IEnumerable GetModsFor(ModType type) + { + if (!HasImplementations) + throw new NotImplementedException(); + + return Array.Empty(); + } + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) + { + if (!HasImplementations) + throw new NotImplementedException(); + + return new DrawableOsuRuleset(new OsuRuleset(), beatmap, mods); + } + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) + { + if (!HasImplementations) + throw new NotImplementedException(); + + return new OsuBeatmapConverter(beatmap, new OsuRuleset()); + } + + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) + { + if (!HasImplementations) + throw new NotImplementedException(); + + return new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap); + } + + public override string Description => "outdated ruleset"; + public override string ShortName => "ruleset-outdated"; + } } } From 892f43da433a96d408f8b8d723d03c677d1d3653 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 14:28:55 +0900 Subject: [PATCH 30/52] Add test coverage of ruleset being marked unavailable if methods are throwing --- osu.Game.Tests/Database/RulesetStoreTests.cs | 35 +++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index dedec6dc83..a5662fa121 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -60,15 +60,40 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestRulesetThrowingOnMethods() + { + RunTestWithRealm((realm, storage) => + { + LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; + LoadTestRuleset.HasImplementations = false; + + var ruleset = new LoadTestRuleset(); + string rulesetShortName = ruleset.RulesetInfo.ShortName; + + realm.Write(r => r.Add(new RulesetInfo(rulesetShortName, ruleset.RulesetInfo.Name, ruleset.RulesetInfo.InstantiationInfo, ruleset.RulesetInfo.OnlineID) + { + Available = true, + })); + + Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + + // Availability is updated on construction of a RealmRulesetStore + var _ = new RealmRulesetStore(realm, storage); + + Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.False); + }); + } + [Test] public void TestOutdatedRulesetNotAvailable() { RunTestWithRealm((realm, storage) => { - OutdatedRuleset.Version = "2021.101.0"; - OutdatedRuleset.HasImplementations = true; + LoadTestRuleset.Version = "2021.101.0"; + LoadTestRuleset.HasImplementations = true; - var ruleset = new OutdatedRuleset(); + var ruleset = new LoadTestRuleset(); string rulesetShortName = ruleset.RulesetInfo.ShortName; realm.Write(r => r.Add(new RulesetInfo(rulesetShortName, ruleset.RulesetInfo.Name, ruleset.RulesetInfo.InstantiationInfo, ruleset.RulesetInfo.OnlineID) @@ -84,14 +109,14 @@ namespace osu.Game.Tests.Database Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.False); // Simulate the ruleset getting updated - OutdatedRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; + LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; var __ = new RealmRulesetStore(realm, storage); Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); }); } - private class OutdatedRuleset : Ruleset + private class LoadTestRuleset : Ruleset { public override string RulesetAPIVersionSupported => Version; From e8ae6840ea6da67cb206e55955c1040951378769 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 15:23:34 +0900 Subject: [PATCH 31/52] Add test coverage of selection being retained --- .../SongSelect/TestScenePlaySongSelect.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 3d8f496c9a..5db46e3097 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -6,15 +6,18 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; @@ -24,6 +27,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -413,6 +417,55 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); } + [Test] + public void TestSelectionRetainedOnBeatmapUpdate() + { + createSongSelect(); + changeRuleset(0); + + Live original = null!; + int originalOnlineSetID = 0; + + AddStep(@"Sort by artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist)); + + AddStep("import original", () => + { + original = manager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely(); + originalOnlineSetID = original!.Value.OnlineID; + }); + + // This will move the beatmap set to a different location in the carousel. + AddStep("Update original with bogus info", () => + { + original.PerformWrite(set => + { + foreach (var beatmap in set.Beatmaps) + { + beatmap.Metadata.Artist = "ZZZZZ"; + beatmap.OnlineID = 12804; + } + }); + }); + + AddRepeatStep("import other beatmaps", () => + { + var testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(); + + foreach (var beatmap in testBeatmapSetInfo.Beatmaps) + beatmap.Metadata.Artist = ((char)RNG.Next('A', 'Z')).ToString(); + + manager.Import(testBeatmapSetInfo); + }, 10); + + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID); + + Task> updateTask = null!; + AddStep("update beatmap", () => updateTask = manager.ImportAsUpdate(new ProgressNotification(), new ImportTask(TestResources.GetQuickTestBeatmapForImport()), original.Value)); + AddUntilStep("wait for update completion", () => updateTask.IsCompleted); + + AddUntilStep("retained selection", () => songSelect.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID); + } + [Test] public void TestPresentNewRulesetNewBeatmap() { From b2e80ca7f0314f7478b340309632a900f5119ace Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 29 Aug 2022 15:27:19 +0900 Subject: [PATCH 32/52] Don't include misses in failed score statistics --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 5b21caee84..547b132345 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -468,22 +468,6 @@ namespace osu.Game.Rulesets.Scoring score.Passed = false; Rank.Value = ScoreRank.F; - Debug.Assert(maximumResultCounts != null); - - if (maximumResultCounts.TryGetValue(HitResult.LargeTickHit, out int maximumLargeTick)) - scoreResultCounts[HitResult.LargeTickMiss] = maximumLargeTick - scoreResultCounts.GetValueOrDefault(HitResult.LargeTickHit); - - if (maximumResultCounts.TryGetValue(HitResult.SmallTickHit, out int maximumSmallTick)) - scoreResultCounts[HitResult.SmallTickMiss] = maximumSmallTick - scoreResultCounts.GetValueOrDefault(HitResult.SmallTickHit); - - int maximumBonusOrIgnore = maximumResultCounts.Where(kvp => kvp.Key.IsBonus() || kvp.Key == HitResult.IgnoreHit).Sum(kvp => kvp.Value); - int currentBonusOrIgnore = scoreResultCounts.Where(kvp => kvp.Key.IsBonus() || kvp.Key == HitResult.IgnoreHit).Sum(kvp => kvp.Value); - scoreResultCounts[HitResult.IgnoreMiss] = maximumBonusOrIgnore - currentBonusOrIgnore; - - int maximumBasic = maximumResultCounts.SingleOrDefault(kvp => kvp.Key.IsBasic()).Value; - int currentBasic = scoreResultCounts.Where(kvp => kvp.Key.IsBasic() && kvp.Key != HitResult.Miss).Sum(kvp => kvp.Value); - scoreResultCounts[HitResult.Miss] = maximumBasic - currentBasic; - PopulateScore(score); } From 423f6f90f2d28bf81ff5096b4b3a8156e7ad9caa Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Aug 2022 21:31:30 +0900 Subject: [PATCH 33/52] Remove async calls from ScoreManager --- .../TestScenePlayerLocalScoreImport.cs | 2 +- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 2 +- .../SongSelect/TestSceneTopLocalRank.cs | 2 +- .../TestSceneDeleteLocalScore.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- .../BeatmapSet/Scores/ScoresContainer.cs | 28 ++--- osu.Game/Scoring/ScoreManager.cs | 101 ++---------------- .../Playlists/PlaylistsResultsScreen.cs | 33 +++--- .../Expanded/ExpandedPanelMiddleContent.cs | 4 +- osu.Game/Screens/Ranking/ScorePanelList.cs | 45 ++++---- .../Select/Leaderboards/BeatmapLeaderboard.cs | 28 ++--- 11 files changed, 62 insertions(+), 187 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index ddb585a73c..f3e436e31f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay { Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(new ScoreManager(rulesets, () => beatmaps, LocalStorage, Realm, Scheduler, API)); + Dependencies.Cache(new ScoreManager(rulesets, () => beatmaps, LocalStorage, Realm, API)); Dependencies.Cache(Realm); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index aeb30c94e1..07da1790c8 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.SongSelect dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); - dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, Scheduler, API)); + dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); Dependencies.Cache(Realm); return dependencies; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs index 72d78ededb..086af3084d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.SongSelect { Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Realm, Scheduler, API)); + Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Realm, API)); Dependencies.Cache(Realm); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 3beade9d4f..db380cfdb7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.UserInterface dependencies.Cache(new RealmRulesetStore(Realm)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); - dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Realm, Scheduler, API)); + dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Realm, API)); Dependencies.Cache(Realm); return dependencies; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8d5c58d5f0..f87f95efd5 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -273,7 +273,7 @@ namespace osu.Game dependencies.Cache(difficultyCache = new BeatmapDifficultyCache()); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() - dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, API, difficultyCache, LocalConfig)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, API, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true)); diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index e50fc356eb..53818bbee3 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -6,10 +6,8 @@ using System.Diagnostics; using System.Linq; using System.Threading; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -87,27 +85,19 @@ namespace osu.Game.Overlays.BeatmapSet.Scores MD5Hash = apiBeatmap.MD5Hash }; - scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo)).ToArray(), loadCancellationSource.Token) - .ContinueWith(task => Schedule(() => - { - if (loadCancellationSource.IsCancellationRequested) - return; + var scores = scoreManager.OrderByTotalScore(value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo))).ToArray(); + var topScore = scores.First(); - var scores = task.GetResultSafely(); + scoreTable.DisplayScores(scores, apiBeatmap.Status.GrantsPerformancePoints()); + scoreTable.Show(); - var topScore = scores.First(); + var userScore = value.UserScore; + var userScoreInfo = userScore?.Score.ToScoreInfo(rulesets, beatmapInfo); - scoreTable.DisplayScores(scores, apiBeatmap.Status.GrantsPerformancePoints()); - scoreTable.Show(); + topScoresContainer.Add(new DrawableTopScore(topScore)); - var userScore = value.UserScore; - var userScoreInfo = userScore?.Score.ToScoreInfo(rulesets, beatmapInfo); - - topScoresContainer.Add(new DrawableTopScore(topScore)); - - if (userScoreInfo != null && userScoreInfo.OnlineID != topScore.OnlineID) - topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position)); - }), TaskContinuationOptions.OnlyOnRanToCompletion); + if (userScoreInfo != null && userScoreInfo.OnlineID != topScore.OnlineID) + topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position)); }); } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index ecd37c761c..7204b5a281 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -11,10 +11,7 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Bindables; -using osu.Framework.Extensions; -using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; @@ -28,17 +25,12 @@ namespace osu.Game.Scoring { public class ScoreManager : ModelManager, IModelImporter { - private readonly Scheduler scheduler; - private readonly BeatmapDifficultyCache difficultyCache; private readonly OsuConfigManager configManager; private readonly ScoreImporter scoreImporter; - public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler, IAPIProvider api, - BeatmapDifficultyCache difficultyCache = null, OsuConfigManager configManager = null) + public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, IAPIProvider api, OsuConfigManager configManager = null) : base(storage, realm) { - this.scheduler = scheduler; - this.difficultyCache = difficultyCache; this.configManager = configManager; scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm, api) @@ -63,27 +55,13 @@ namespace osu.Game.Scoring /// Orders an array of s by total score. /// /// The array of s to reorder. - /// A to cancel the process. /// The given ordered by decreasing total score. - public async Task OrderByTotalScoreAsync(ScoreInfo[] scores, CancellationToken cancellationToken = default) + public IEnumerable OrderByTotalScore(IEnumerable scores) { - if (difficultyCache != null) - { - // Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below. - foreach (var s in scores) - { - await difficultyCache.GetDifficultyAsync(s.BeatmapInfo, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - } - } - - long[] totalScores = await Task.WhenAll(scores.Select(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken))).ConfigureAwait(false); - - return scores.Select((score, index) => (score, totalScore: totalScores[index])) + return scores.Select((score, index) => (score, totalScore: GetTotalScore(score))) .OrderByDescending(g => g.totalScore) .ThenBy(g => g.score.OnlineID) - .Select(g => g.score) - .ToArray(); + .Select(g => g.score); } /// @@ -106,44 +84,18 @@ namespace osu.Game.Scoring /// The bindable containing the formatted total score string. public Bindable GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score)); - /// - /// Retrieves the total score of a in the given . - /// The score is returned in a callback that is run on the update thread. - /// - /// The to calculate the total score of. - /// The callback to be invoked with the total score. - /// The to return the total score as. - /// A to cancel the process. - public void GetTotalScore([NotNull] ScoreInfo score, [NotNull] Action callback, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default) - { - GetTotalScoreAsync(score, mode, cancellationToken) - .ContinueWith(task => scheduler.Add(() => - { - if (!cancellationToken.IsCancellationRequested) - callback(task.GetResultSafely()); - }), TaskContinuationOptions.OnlyOnRanToCompletion); - } - /// /// Retrieves the total score of a in the given . /// /// The to calculate the total score of. /// The to return the total score as. - /// A to cancel the process. /// The total score. - public async Task GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default) + public long GetTotalScore([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised) { // TODO: This is required for playlist aggregate scores. They should likely not be getting here in the first place. if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash)) return score.TotalScore; - int? beatmapMaxCombo = await GetMaximumAchievableComboAsync(score, cancellationToken).ConfigureAwait(false); - if (beatmapMaxCombo == null) - return score.TotalScore; - - if (beatmapMaxCombo == 0) - return 0; - var ruleset = score.Ruleset.CreateInstance(); var scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = score.Mods; @@ -155,33 +107,9 @@ namespace osu.Game.Scoring /// Retrieves the maximum achievable combo for the provided score. /// /// The to compute the maximum achievable combo for. - /// A to cancel the process. /// The maximum achievable combo. A return value indicates the difficulty cache has failed to retrieve the combo. - public async Task GetMaximumAchievableComboAsync([NotNull] ScoreInfo score, CancellationToken cancellationToken = default) + public int GetMaximumAchievableCombo([NotNull] ScoreInfo score) { - if (score.IsLegacyScore) - { - // This score is guaranteed to be an osu!stable score. - // The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used. -#pragma warning disable CS0618 - if (score.BeatmapInfo.MaxCombo != null) - return score.BeatmapInfo.MaxCombo.Value; -#pragma warning restore CS0618 - - if (difficultyCache == null) - return null; - - // We can compute the max combo locally after the async beatmap difficulty computation. - var difficulty = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false); - - if (difficulty == null) - Logger.Log($"Couldn't get beatmap difficulty for beatmap {score.BeatmapInfo.OnlineID}"); - - return difficulty?.MaxCombo; - } - - // This is guaranteed to be a non-legacy score. - // The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values. return Enum.GetValues(typeof(HitResult)).OfType().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum(); } @@ -191,10 +119,6 @@ namespace osu.Game.Scoring private class TotalScoreBindable : Bindable { private readonly Bindable scoringMode = new Bindable(); - private readonly ScoreInfo score; - private readonly ScoreManager scoreManager; - - private CancellationTokenSource difficultyCalculationCancellationSource; /// /// Creates a new . @@ -204,19 +128,8 @@ namespace osu.Game.Scoring /// The config. public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager, OsuConfigManager configManager) { - this.score = score; - this.scoreManager = scoreManager; - configManager?.BindWith(OsuSetting.ScoreDisplayMode, scoringMode); - scoringMode.BindValueChanged(onScoringModeChanged, true); - } - - private void onScoringModeChanged(ValueChangedEvent mode) - { - difficultyCalculationCancellationSource?.Cancel(); - difficultyCalculationCancellationSource = new CancellationTokenSource(); - - scoreManager.GetTotalScore(score, s => Value = s, mode.NewValue, difficultyCalculationCancellationSource.Token); + scoringMode.BindValueChanged(mode => Value = scoreManager.GetTotalScore(score, mode.NewValue), true); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs index 53b38962ac..41633c34ce 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs @@ -180,31 +180,26 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The callback to invoke with the final s. /// The s that were retrieved from s. /// An optional pivot around which the scores were retrieved. - private void performSuccessCallback([NotNull] Action> callback, [NotNull] List scores, [CanBeNull] MultiplayerScores pivot = null) + private void performSuccessCallback([NotNull] Action> callback, [NotNull] List scores, [CanBeNull] MultiplayerScores pivot = null) => Schedule(() => { - var scoreInfos = scores.Select(s => s.CreateScoreInfo(rulesets, playlistItem, Beatmap.Value.BeatmapInfo)).ToArray(); + var scoreInfos = scoreManager.OrderByTotalScore(scores.Select(s => s.CreateScoreInfo(rulesets, playlistItem, Beatmap.Value.BeatmapInfo))).ToArray(); - // Score panels calculate total score before displaying, which can take some time. In order to count that calculation as part of the loading spinner display duration, - // calculate the total scores locally before invoking the success callback. - scoreManager.OrderByTotalScoreAsync(scoreInfos).ContinueWith(_ => Schedule(() => + // Select a score if we don't already have one selected. + // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). + if (SelectedScore.Value == null) { - // Select a score if we don't already have one selected. - // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). - if (SelectedScore.Value == null) + Schedule(() => { - Schedule(() => - { - // Prefer selecting the local user's score, or otherwise default to the first visible score. - SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault(); - }); - } + // Prefer selecting the local user's score, or otherwise default to the first visible score. + SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault(); + }); + } - // Invoke callback to add the scores. Exclude the user's current score which was added previously. - callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); + // Invoke callback to add the scores. Exclude the user's current score which was added previously. + callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); - hideLoadingSpinners(pivot); - })); - } + hideLoadingSpinners(pivot); + }); private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null) { diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 0f202e5e08..b496f4242d 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -67,12 +67,10 @@ namespace osu.Game.Screens.Ranking.Expanded var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; string creator = metadata.Author.Username; - int? beatmapMaxCombo = scoreManager.GetMaximumAchievableComboAsync(score).GetResultSafely(); - var topStatistics = new List { new AccuracyStatistic(score.Accuracy), - new ComboStatistic(score.MaxCombo, beatmapMaxCombo), + new ComboStatistic(score.MaxCombo, scoreManager.GetMaximumAchievableCombo(score)), new PerformanceStatistic(score), }; diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 4f9e61a4a1..46f9efd126 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -8,11 +8,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; @@ -151,32 +149,27 @@ namespace osu.Game.Screens.Ranking var score = trackingContainer.Panel.Score; - // Calculating score can take a while in extreme scenarios, so only display scores after the process completes. - scoreManager.GetTotalScoreAsync(score) - .ContinueWith(task => Schedule(() => - { - flow.SetLayoutPosition(trackingContainer, task.GetResultSafely()); + flow.SetLayoutPosition(trackingContainer, scoreManager.GetTotalScore(score)); - trackingContainer.Show(); + trackingContainer.Show(); - if (SelectedScore.Value?.Equals(score) == true) - { - SelectedScore.TriggerChange(); - } - else - { - // We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done. - // But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel. - if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score)) - { - // A somewhat hacky property is used here because we need to: - // 1) Scroll after the scroll container's visible range is updated. - // 2) Scroll before the scroll container's scroll position is updated. - // Without this, we would have a 1-frame positioning error which looks very jarring. - scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing; - } - } - }), TaskContinuationOptions.OnlyOnRanToCompletion); + if (SelectedScore.Value?.Equals(score) == true) + { + SelectedScore.TriggerChange(); + } + else + { + // We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done. + // But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel. + if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score)) + { + // A somewhat hacky property is used here because we need to: + // 1) Scroll after the scroll container's visible range is updated. + // 2) Scroll before the scroll container's scroll position is updated. + // Without this, we would have a 1-frame positioning error which looks very jarring. + scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing; + } + } } /// diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index b497943dfa..343b815e9f 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -8,10 +8,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Extensions; @@ -150,17 +148,12 @@ namespace osu.Game.Screens.Select.Leaderboards var req = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods); - req.Success += r => + req.Success += r => Schedule(() => { - scoreManager.OrderByTotalScoreAsync(r.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo)).ToArray(), cancellationToken) - .ContinueWith(task => Schedule(() => - { - if (cancellationToken.IsCancellationRequested) - return; - - SetScores(task.GetResultSafely(), r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo)); - }), TaskContinuationOptions.OnlyOnRanToCompletion); - }; + SetScores( + scoreManager.OrderByTotalScore(r.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo))), + r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo)); + }); return req; } @@ -213,16 +206,9 @@ namespace osu.Game.Screens.Select.Leaderboards scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym))); } - scores = scores.Detach(); + scores = scoreManager.OrderByTotalScore(scores.Detach()); - scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken) - .ContinueWith(ordered => Schedule(() => - { - if (cancellationToken.IsCancellationRequested) - return; - - SetScores(ordered.GetResultSafely()); - }), TaskContinuationOptions.OnlyOnRanToCompletion); + Schedule(() => SetScores(scores)); } } From d75543ad68acc74546d9e4ab4d1c2066efb52d44 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 29 Aug 2022 15:36:10 +0900 Subject: [PATCH 34/52] Simplify GetMaximumAchievableCombo further --- osu.Game/Scoring/ScoreManager.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 7204b5a281..fd600f4864 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -107,11 +107,8 @@ namespace osu.Game.Scoring /// Retrieves the maximum achievable combo for the provided score. /// /// The to compute the maximum achievable combo for. - /// The maximum achievable combo. A return value indicates the difficulty cache has failed to retrieve the combo. - public int GetMaximumAchievableCombo([NotNull] ScoreInfo score) - { - return Enum.GetValues(typeof(HitResult)).OfType().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum(); - } + /// The maximum achievable combo. + public int GetMaximumAchievableCombo([NotNull] ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value); /// /// Provides the total score of a . Responds to changes in the currently-selected . From 81ac0daba8cd9574fd08821cbf8fe767d0969a1b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 29 Aug 2022 15:51:12 +0900 Subject: [PATCH 35/52] Update xmldoc --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 547b132345..cd01ae7eff 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -458,7 +458,7 @@ namespace osu.Game.Rulesets.Scoring } /// - /// Populates the given score with remaining statistics as "missed" and marks it with rank. + /// Populates a failed score, marking it with the rank. /// public void FailScore(ScoreInfo score) { From d4a37725c43d6582b89c2eba96d5732ee265785a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 29 Aug 2022 15:59:57 +0900 Subject: [PATCH 36/52] Adjust test --- .../Gameplay/TestSceneScoreProcessor.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index 4123412ab6..fb9d841d99 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -124,14 +124,19 @@ namespace osu.Game.Tests.Gameplay Assert.That(score.Rank, Is.EqualTo(ScoreRank.F)); Assert.That(score.Passed, Is.False); - Assert.That(score.Statistics.Count(kvp => kvp.Value > 0), Is.EqualTo(7)); + Assert.That(score.Statistics.Sum(kvp => kvp.Value), Is.EqualTo(4)); + Assert.That(score.MaximumStatistics.Sum(kvp => kvp.Value), Is.EqualTo(8)); + Assert.That(score.Statistics[HitResult.Ok], Is.EqualTo(1)); - Assert.That(score.Statistics[HitResult.Miss], Is.EqualTo(1)); Assert.That(score.Statistics[HitResult.LargeTickHit], Is.EqualTo(1)); - Assert.That(score.Statistics[HitResult.LargeTickMiss], Is.EqualTo(1)); - Assert.That(score.Statistics[HitResult.SmallTickMiss], Is.EqualTo(2)); + Assert.That(score.Statistics[HitResult.SmallTickMiss], Is.EqualTo(1)); Assert.That(score.Statistics[HitResult.SmallBonus], Is.EqualTo(1)); - Assert.That(score.Statistics[HitResult.IgnoreMiss], Is.EqualTo(1)); + + Assert.That(score.MaximumStatistics[HitResult.Perfect], Is.EqualTo(2)); + Assert.That(score.MaximumStatistics[HitResult.LargeTickHit], Is.EqualTo(2)); + Assert.That(score.MaximumStatistics[HitResult.SmallTickHit], Is.EqualTo(2)); + Assert.That(score.MaximumStatistics[HitResult.SmallBonus], Is.EqualTo(1)); + Assert.That(score.MaximumStatistics[HitResult.LargeBonus], Is.EqualTo(1)); } private class TestJudgement : Judgement From be5c6232e82ea1fa9115fda102a6259ec5bb2444 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 16:29:19 +0900 Subject: [PATCH 37/52] Encapsulate `Track` inside a `FramedClock` to avoid mutating operations --- osu.Game/OsuGameBase.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 27ea3a76ae..0b158d5e08 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -381,7 +381,16 @@ namespace osu.Game Beatmap.BindValueChanged(onBeatmapChanged); } - private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) => beatmapClock.ChangeSource(beatmap.Track); + private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) + { + // FramedBeatmapClock uses a decoupled clock internally which will mutate the source if it is an `IAdjustableClock`. + // We don't want this for now, as the intention of beatmapClock is to be a read-only source for beat sync components. + // + // Encapsulating in a FramedClock will avoid any mutations. + var framedClock = new FramedClock(beatmap.Track); + + beatmapClock.ChangeSource(framedClock); + } protected virtual void InitialiseFonts() { From cf8fad045d94a010d96c7764fc66879ae2f22561 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 16:32:41 +0900 Subject: [PATCH 38/52] Update template rulesets to include baked value --- .../osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs | 3 +++ .../osu.Game.Rulesets.Pippidon/PippidonRuleset.cs | 3 +++ .../osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs | 3 +++ .../osu.Game.Rulesets.Pippidon/PippidonRuleset.cs | 3 +++ 4 files changed, 12 insertions(+) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs index 00754c6346..1e88f87f09 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs @@ -77,5 +77,8 @@ namespace osu.Game.Rulesets.EmptyFreeform }; } } + + // Leave this line intact. It will bake the correct version into the ruleset on each build/release. + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; } } diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs index 0522840e9e..2f6ba0dda6 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs @@ -49,5 +49,8 @@ namespace osu.Game.Rulesets.Pippidon }; public override Drawable CreateIcon() => new PippidonRulesetIcon(this); + + // Leave this line intact. It will bake the correct version into the ruleset on each build/release. + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; } } diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs index c8f0c07724..a32586c414 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs @@ -54,5 +54,8 @@ namespace osu.Game.Rulesets.EmptyScrolling Text = ShortName[0].ToString(), Font = OsuFont.Default.With(size: 18), }; + + // Leave this line intact. It will bake the correct version into the ruleset on each build/release. + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; } } diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs index 89246373ee..bde530feb8 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs @@ -46,5 +46,8 @@ namespace osu.Game.Rulesets.Pippidon }; public override Drawable CreateIcon() => new PippidonRulesetIcon(this); + + // Leave this line intact. It will bake the correct version into the ruleset on each build/release. + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; } } From 2dafa041a7e0a732159691f2107977555378e836 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 16:42:50 +0900 Subject: [PATCH 39/52] Account for offset being applied to editor clock time in `TestSceneEditorClock` --- osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs | 2 +- osu.Game/Beatmaps/FramedBeatmapClock.cs | 4 ++-- osu.Game/Screens/Edit/EditorClock.cs | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs index d598ebafa9..319f8ab9dc 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("seek near end", () => EditorClock.Seek(EditorClock.TrackLength - 250)); AddUntilStep("clock stops", () => !EditorClock.IsRunning); - AddUntilStep("clock stopped at end", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength)); + AddUntilStep("clock stopped at end", () => EditorClock.CurrentTime - EditorClock.TotalAppliedOffset, () => Is.EqualTo(EditorClock.TrackLength)); AddStep("start clock again", () => EditorClock.Start()); AddAssert("clock looped to start", () => EditorClock.IsRunning && EditorClock.CurrentTime < 500); diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index c86f25640f..a4787a34e8 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -124,7 +124,7 @@ namespace osu.Game.Beatmaps finalClockSource.ProcessFrame(); } - private double totalAppliedOffset + public double TotalAppliedOffset { get { @@ -169,7 +169,7 @@ namespace osu.Game.Beatmaps public bool Seek(double position) { - bool success = decoupledClock.Seek(position - totalAppliedOffset); + bool success = decoupledClock.Seek(position - TotalAppliedOffset); finalClockSource.ProcessFrame(); return success; diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 7a00a74530..6485f683ad 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -158,6 +158,8 @@ namespace osu.Game.Screens.Edit public double CurrentTime => underlyingClock.CurrentTime; + public double TotalAppliedOffset => underlyingClock.TotalAppliedOffset; + public void Reset() { ClearTransforms(); From 780121eeee359a433aba22dba057fcc2a55ef8d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 17:12:09 +0900 Subject: [PATCH 40/52] Add setting to toggle metronome in "Target" mod As mentioned in https://github.com/ppy/osu/discussions/20006#discussioncomment-3496732. --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 82260db818..b48f0b4ccd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -59,6 +59,9 @@ namespace osu.Game.Rulesets.Osu.Mods Value = null }; + [SettingSource("Metronome ticks", "Whether a metronome beat should play in the background")] + public BindableBool Metronome { get; } = new BindableBool(true); + #region Constants /// @@ -337,7 +340,8 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - drawableRuleset.Overlays.Add(new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime)); + if (Metronome.Value) + drawableRuleset.Overlays.Add(new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime)); } #endregion From 07b502f69af5bc73b84ac0df689fec121edc39f7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 29 Aug 2022 17:58:57 +0900 Subject: [PATCH 41/52] Simplify OrderByTotalScore implementation --- osu.Game/Scoring/ScoreManager.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index fd600f4864..782590114f 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -57,12 +57,7 @@ namespace osu.Game.Scoring /// The array of s to reorder. /// The given ordered by decreasing total score. public IEnumerable OrderByTotalScore(IEnumerable scores) - { - return scores.Select((score, index) => (score, totalScore: GetTotalScore(score))) - .OrderByDescending(g => g.totalScore) - .ThenBy(g => g.score.OnlineID) - .Select(g => g.score); - } + => scores.OrderByDescending(s => GetTotalScore(s)).ThenBy(s => s.OnlineID); /// /// Retrieves a bindable that represents the total score of a . From 3eda284b03b5abcabe78b03d743078313f8f52a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 18:17:41 +0900 Subject: [PATCH 42/52] Always reprocess beatmaps after a user update request This covers the rare case where metadata may have changed server-side but not the beatmap itself. Tested with the provided user database to resolve the issue. Closes #19976. --- osu.Game/Beatmaps/BeatmapImporter.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 0fa30cf5e7..292caa4397 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -55,7 +55,14 @@ namespace osu.Game.Beatmaps // If there were no changes, ensure we don't accidentally nuke ourselves. if (first.ID == original.ID) + { + first.PerformRead(s => + { + // Re-run processing even in this case. We might have outdated metadata. + ProcessBeatmap?.Invoke((s, false)); + }); return first; + } first.PerformWrite(updated => { From d50e9caa11a5930d49b39aa4ce18e11827730f7f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 29 Aug 2022 18:58:29 +0200 Subject: [PATCH 43/52] Moved guards to separate canMerge method --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 8b67c0dcc9..dd9d1614b8 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -354,12 +354,14 @@ namespace osu.Game.Rulesets.Osu.Edit .OrderBy(h => h.StartTime) .ToArray(); + private bool canMerge(IReadOnlyList objects) => + objects.Count > 1 && (objects.Any(h => h is Slider) || Precision.DefinitelyBigger(Vector2.DistanceSquared(objects[0].Position, objects[1].Position), 1)); + private void mergeSelection() { var mergeableObjects = selectedMergeableObjects; - if (mergeableObjects.Length < 2 || (mergeableObjects.All(h => h is not Slider) - && Precision.AlmostBigger(1, Vector2.DistanceSquared(mergeableObjects[0].Position, mergeableObjects[1].Position)))) + if (!canMerge(mergeableObjects)) return; ChangeHandler?.BeginChange(); @@ -446,9 +448,7 @@ namespace osu.Game.Rulesets.Osu.Edit foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; - var mergeableObjects = selectedMergeableObjects; - if (mergeableObjects.Length > 1 && (mergeableObjects.Any(h => h is Slider) - || Precision.DefinitelyBigger(Vector2.DistanceSquared(mergeableObjects[0].Position, mergeableObjects[1].Position), 1))) + if (canMerge(selectedMergeableObjects)) yield return new OsuMenuItem("Merge selection", MenuItemType.Destructive, mergeSelection); } } From 2e5770be4e14623f61327063f62019112d15e316 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 02:51:42 +0900 Subject: [PATCH 44/52] Move helper method to bottom of class --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index dd9d1614b8..048fd4ed7e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -354,9 +354,6 @@ namespace osu.Game.Rulesets.Osu.Edit .OrderBy(h => h.StartTime) .ToArray(); - private bool canMerge(IReadOnlyList objects) => - objects.Count > 1 && (objects.Any(h => h is Slider) || Precision.DefinitelyBigger(Vector2.DistanceSquared(objects[0].Position, objects[1].Position), 1)); - private void mergeSelection() { var mergeableObjects = selectedMergeableObjects; @@ -451,5 +448,10 @@ namespace osu.Game.Rulesets.Osu.Edit if (canMerge(selectedMergeableObjects)) yield return new OsuMenuItem("Merge selection", MenuItemType.Destructive, mergeSelection); } + + private bool canMerge(IReadOnlyList objects) => + objects.Count > 1 + && (objects.Any(h => h is Slider) + || Precision.DefinitelyBigger(Vector2.DistanceSquared(objects[0].Position, objects[1].Position), 1)); } } From 44916c51d73580ab42c5a18f33705da9ffae9a16 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 30 Aug 2022 00:18:55 +0200 Subject: [PATCH 45/52] Updated canMerge check to be totally accurate --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 048fd4ed7e..ac5fad54a4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -452,6 +452,6 @@ namespace osu.Game.Rulesets.Osu.Edit private bool canMerge(IReadOnlyList objects) => objects.Count > 1 && (objects.Any(h => h is Slider) - || Precision.DefinitelyBigger(Vector2.DistanceSquared(objects[0].Position, objects[1].Position), 1)); + || objects.Zip(objects.Skip(1), (h1, h2) => Precision.DefinitelyBigger(Vector2.DistanceSquared(h1.Position, h2.Position), 1)).Any(x => x)); } } From 5d41fdfc890acd4d9dc1d45e3e96f890ec12f1b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 15:05:21 +0900 Subject: [PATCH 46/52] Remove unnecessary usage of `DrawableAudioMixer` in `ScorePanel` --- osu.Game/Screens/Ranking/ScorePanel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 0bcfa0da1f..cb777de144 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -99,7 +99,7 @@ namespace osu.Game.Screens.Ranking [Resolved] private OsuGameBase game { get; set; } - private DrawableAudioMixer mixer; + private AudioContainer audioContent; private bool displayWithFlair; @@ -130,7 +130,7 @@ namespace osu.Game.Screens.Ranking // Adding a manual offset here allows the expanded version to take on an "acceptable" vertical centre when at 100% UI scale. const float vertical_fudge = 20; - InternalChild = mixer = new DrawableAudioMixer + InternalChild = audioContent = new AudioContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -225,7 +225,7 @@ namespace osu.Game.Screens.Ranking protected override void Update() { base.Update(); - mixer.Balance.Value = (ScreenSpaceDrawQuad.Centre.X / game.ScreenSpaceDrawQuad.Width) * 2 - 1; + audioContent.Balance.Value = (ScreenSpaceDrawQuad.Centre.X / game.ScreenSpaceDrawQuad.Width) * 2 - 1; } private void playAppearSample() @@ -274,7 +274,7 @@ namespace osu.Game.Screens.Ranking break; } - mixer.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint); + audioContent.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint); bool topLayerExpanded = topLayerContainer.Y < 0; From 5202c15a0e3edc10e3667805c0db15fe059e441b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 30 Aug 2022 15:11:39 +0900 Subject: [PATCH 47/52] Populate MaximumStatistics for test scores --- osu.Game.Tests/Resources/TestResources.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index 6bce03869d..9c85f61330 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -194,8 +194,16 @@ namespace osu.Game.Tests.Resources [HitResult.LargeTickHit] = 100, [HitResult.LargeTickMiss] = 50, [HitResult.SmallBonus] = 10, - [HitResult.SmallBonus] = 50 + [HitResult.LargeBonus] = 50 }, + MaximumStatistics = new Dictionary + { + [HitResult.Perfect] = 971, + [HitResult.SmallTickHit] = 75, + [HitResult.LargeTickHit] = 150, + [HitResult.SmallBonus] = 10, + [HitResult.LargeBonus] = 50, + } }; private class TestModHardRock : ModHardRock From 8b3742188fcb9c4c80c7515546091b2ec714ec3e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 30 Aug 2022 15:42:33 +0900 Subject: [PATCH 48/52] Fix test by also clearing out maximum statistics --- .../Visual/Playlists/TestScenePlaylistsResultsScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 8a04cd96fe..26fa740159 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -61,6 +61,7 @@ namespace osu.Game.Tests.Visual.Playlists userScore = TestResources.CreateTestScoreInfo(); userScore.TotalScore = 0; userScore.Statistics = new Dictionary(); + userScore.MaximumStatistics = new Dictionary(); bindHandler(); From 799c015bff82460ffb1c1392610d47984281a443 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 30 Aug 2022 15:50:19 +0900 Subject: [PATCH 49/52] Add LegacyTotalScore to SoloScoreInfo --- osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index 16aa800cb0..a8cedabd48 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -77,6 +77,12 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("maximum_statistics")] public Dictionary MaximumStatistics { get; set; } = new Dictionary(); + /// + /// Used to preserve the total score for legacy scores. + /// + [JsonProperty("legacy_total_score")] + public int? LegacyTotalScore { get; set; } + #region osu-web API additions (not stored to database). [JsonProperty("id")] From b8fda1a16f3668e244086ba5958f9b818c95c02b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 15:51:46 +0900 Subject: [PATCH 50/52] Apply NRT to notification classes and tidy things up a bit. --- .../TestSceneNotificationOverlay.cs | 10 +++--- .../Database/ImportProgressNotification.cs | 2 -- osu.Game/Overlays/NotificationOverlay.cs | 12 ++----- .../Overlays/Notifications/Notification.cs | 8 ++--- .../Notifications/NotificationSection.cs | 14 ++++---- .../Notifications/ProgressNotification.cs | 32 +++++++++---------- .../Notifications/SimpleNotification.cs | 2 -- 7 files changed, 32 insertions(+), 48 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index cf069a9e34..c196b204b9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -19,11 +17,11 @@ namespace osu.Game.Tests.Visual.UserInterface [TestFixture] public class TestSceneNotificationOverlay : OsuTestScene { - private NotificationOverlay notificationOverlay; + private NotificationOverlay notificationOverlay = null!; private readonly List progressingNotifications = new List(); - private SpriteText displayedCount; + private SpriteText displayedCount = null!; [SetUp] public void SetUp() => Schedule(() => @@ -46,7 +44,7 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestCompleteProgress() { - ProgressNotification notification = null; + ProgressNotification notification = null!; AddStep("add progress notification", () => { notification = new ProgressNotification @@ -64,7 +62,7 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestCancelProgress() { - ProgressNotification notification = null; + ProgressNotification notification = null!; AddStep("add progress notification", () => { notification = new ProgressNotification diff --git a/osu.Game/Database/ImportProgressNotification.cs b/osu.Game/Database/ImportProgressNotification.cs index 46f9936bc2..aaee3e117f 100644 --- a/osu.Game/Database/ImportProgressNotification.cs +++ b/osu.Game/Database/ImportProgressNotification.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Overlays.Notifications; namespace osu.Game.Database diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 281077bcf6..cbcc7b6886 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -64,14 +64,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Children = new[] { - new NotificationSection(AccountsStrings.NotificationsTitle, "Clear All") - { - AcceptTypes = new[] { typeof(SimpleNotification) } - }, - new NotificationSection(@"Running Tasks", @"Cancel All") - { - AcceptTypes = new[] { typeof(ProgressNotification) } - } + new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, "Clear All"), + new NotificationSection(@"Running Tasks", new[] { typeof(ProgressNotification) }, @"Cancel All"), } } } @@ -133,7 +127,7 @@ namespace osu.Game.Overlays var ourType = notification.GetType(); - var section = sections.Children.FirstOrDefault(s => s.AcceptTypes.Any(accept => accept.IsAssignableFrom(ourType))); + var section = sections.Children.FirstOrDefault(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); section?.Add(notification, notification.DisplayOnTop ? -runningDepth : runningDepth); if (notification.IsImportant) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 64bf3693c8..fbb906e637 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -26,7 +24,7 @@ namespace osu.Game.Overlays.Notifications /// /// User requested close. /// - public event Action Closed; + public event Action? Closed; public abstract LocalisableString Text { get; set; } @@ -38,7 +36,7 @@ namespace osu.Game.Overlays.Notifications /// /// Run on user activating the notification. Return true to close. /// - public Func Activated; + public Func? Activated; /// /// Should we show at the top of our section on display? @@ -212,7 +210,7 @@ namespace osu.Game.Overlays.Notifications public class NotificationLight : Container { private bool pulsate; - private Container pulsateLayer; + private Container pulsateLayer = null!; public bool Pulsate { diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index b7e21822fa..d2e18a0cee 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -21,9 +19,9 @@ namespace osu.Game.Overlays.Notifications { public class NotificationSection : AlwaysUpdateFillFlowContainer { - private OsuSpriteText countDrawable; + private OsuSpriteText countDrawable = null!; - private FlowContainer notifications; + private FlowContainer notifications = null!; public int DisplayedCount => notifications.Count(n => !n.WasClosed); public int UnreadCount => notifications.Count(n => !n.WasClosed && !n.Read); @@ -33,14 +31,16 @@ namespace osu.Game.Overlays.Notifications notifications.Insert((int)position, notification); } - public IEnumerable AcceptTypes; + public IEnumerable AcceptedNotificationTypes { get; } private readonly string clearButtonText; private readonly LocalisableString titleText; - public NotificationSection(LocalisableString title, string clearButtonText) + public NotificationSection(LocalisableString title, IEnumerable acceptedNotificationTypes, string clearButtonText) { + AcceptedNotificationTypes = acceptedNotificationTypes.ToArray(); + this.clearButtonText = clearButtonText.ToUpperInvariant(); titleText = title; } @@ -159,7 +159,7 @@ namespace osu.Game.Overlays.Notifications public void MarkAllRead() { - notifications?.Children.ForEach(n => n.Read = true); + notifications.Children.ForEach(n => n.Read = true); } } diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 719e77db83..15346930a3 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Threading; using osu.Framework.Allocation; @@ -25,6 +23,18 @@ namespace osu.Game.Overlays.Notifications { private const float loading_spinner_size = 22; + public Func? CancelRequested { get; set; } + + /// + /// The function to post completion notifications back to. + /// + public Action? CompletionTarget { get; set; } + + /// + /// An action to complete when the completion notification is clicked. Return true to close. + /// + public Func? CompletionClickAction { get; set; } + private LocalisableString text; public override LocalisableString Text @@ -142,7 +152,7 @@ namespace osu.Game.Overlays.Notifications Text = CompletionText }; - protected virtual void Completed() + protected void Completed() { CompletionTarget?.Invoke(CreateCompletionNotification()); base.Close(); @@ -155,8 +165,8 @@ namespace osu.Game.Overlays.Notifications private Color4 colourActive; private Color4 colourCancelled; - private Box iconBackground; - private LoadingSpinner loadingSpinner; + private Box iconBackground = null!; + private LoadingSpinner loadingSpinner = null!; private readonly TextFlowContainer textDrawable; @@ -222,18 +232,6 @@ namespace osu.Game.Overlays.Notifications } } - public Func CancelRequested { get; set; } - - /// - /// The function to post completion notifications back to. - /// - public Action CompletionTarget { get; set; } - - /// - /// An action to complete when the completion notification is clicked. Return true to close. - /// - public Func CompletionClickAction; - private class ProgressBar : Container { private readonly Box box; diff --git a/osu.Game/Overlays/Notifications/SimpleNotification.cs b/osu.Game/Overlays/Notifications/SimpleNotification.cs index fc594902cf..b9a1cc6d90 100644 --- a/osu.Game/Overlays/Notifications/SimpleNotification.cs +++ b/osu.Game/Overlays/Notifications/SimpleNotification.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; From d600058c98419657c7f4b9d630b34ff8b5d2a613 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 18:09:10 +0900 Subject: [PATCH 51/52] Assert non-null in `ProfileHeader` to appease r# --- osu.Game/Overlays/Profile/ProfileHeader.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index f84f829f7a..1eca6a81cf 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -37,6 +38,10 @@ namespace osu.Game.Overlays.Profile // todo: pending implementation. // TabControl.AddItem(LayoutStrings.HeaderUsersModding); + // Haphazardly guaranteed by OverlayHeader constructor (see CreateBackground / CreateContent). + Debug.Assert(centreHeaderContainer != null); + Debug.Assert(detailHeaderContainer != null); + centreHeaderContainer.DetailsVisible.BindValueChanged(visible => detailHeaderContainer.Expanded = visible.NewValue, true); } From 928bce8fcdee3daf785539cd068b48274eae30e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 18:18:46 +0900 Subject: [PATCH 52/52] Fix crash when attempting to watch a replay when the storage file doesn't exist --- osu.Game/Scoring/LegacyDatabasedScore.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Scoring/LegacyDatabasedScore.cs b/osu.Game/Scoring/LegacyDatabasedScore.cs index de35cb5faf..a7641c7999 100644 --- a/osu.Game/Scoring/LegacyDatabasedScore.cs +++ b/osu.Game/Scoring/LegacyDatabasedScore.cs @@ -25,7 +25,12 @@ namespace osu.Game.Scoring return; using (var stream = store.GetStream(replayFilename)) + { + if (stream == null) + return; + Replay = new DatabasedLegacyScoreDecoder(rulesets, beatmaps).Parse(stream).Replay; + } } } }