From 26855a2c04f623e87c1a357e3dab3cda241dd075 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 28 Nov 2023 21:14:34 +0900 Subject: [PATCH 01/59] Add failing test --- .../Formats/LegacyBeatmapDecoderTest.cs | 32 +++++++++++++++++++ .../Resources/custom-slider-length.osu | 19 +++++++++++ 2 files changed, 51 insertions(+) create mode 100644 osu.Game.Tests/Resources/custom-slider-length.osu diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index dcfe8ecb41..02432a1935 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -15,12 +15,14 @@ using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Taiko; using osu.Game.Skinning; using osu.Game.Tests.Resources; using osuTK; @@ -1156,5 +1158,35 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(((IHasComboInformation)playable.HitObjects[17]).ComboIndexWithOffsets, Is.EqualTo(9)); } } + + [Test] + public void TestSliderConversionWithCustomDistance([Values("taiko", "mania")] string rulesetName) + { + using (var resStream = TestResources.OpenResource("custom-slider-length.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + Ruleset ruleset; + + switch (rulesetName) + { + case "taiko": + ruleset = new TaikoRuleset(); + break; + + case "mania": + ruleset = new ManiaRuleset(); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(rulesetName), rulesetName, null); + } + + var decoder = Decoder.GetDecoder(stream); + var working = new TestWorkingBeatmap(decoder.Decode(stream)); + IBeatmap beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty()); + + Assert.That(beatmap.HitObjects[0].GetEndTime(), Is.EqualTo(3153)); + } + } } } diff --git a/osu.Game.Tests/Resources/custom-slider-length.osu b/osu.Game.Tests/Resources/custom-slider-length.osu new file mode 100644 index 0000000000..f7529918a9 --- /dev/null +++ b/osu.Game.Tests/Resources/custom-slider-length.osu @@ -0,0 +1,19 @@ +osu file format v14 + +[General] +Mode: 0 + +[Difficulty] +HPDrainRate:6 +CircleSize:7 +OverallDifficulty:7 +ApproachRate:10 +SliderMultiplier:1.7 +SliderTickRate:1 + +[TimingPoints] +29,333.333333333333,4,1,0,100,1,0 +29,-10000,4,1,0,100,0,0 + +[HitObjects] +256,192,29,6,0,P|384:192|384:192,1,159.375 \ No newline at end of file From 16577829e27696d27f3ae99129a1c9f3a16639d3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 28 Nov 2023 21:14:56 +0900 Subject: [PATCH 02/59] Fix mania and taiko slider conversion distance --- .../Patterns/Legacy/DistanceObjectPatternGenerator.cs | 9 ++++++++- .../Beatmaps/TaikoBeatmapConverter.cs | 10 +++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index cce0944564..8aa128be33 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -60,8 +60,15 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy SpanCount = repeatsData?.SpanCount() ?? 1; StartTime = (int)Math.Round(hitObject.StartTime); + double distance; + + if (hitObject is IHasPath pathData) + distance = pathData.Path.ExpectedDistance.Value ?? 0; + else + distance = distanceData.Distance; + // This matches stable's calculation. - EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier); + EndTime = (int)Math.Floor(StartTime + distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier); SegmentDuration = (EndTime - StartTime) / SpanCount; } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index e46e2ec09c..4827ec76aa 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -182,7 +182,15 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps // The true distance, accounting for any repeats. This ends up being the drum roll distance later int spans = (obj as IHasRepeats)?.SpanCount() ?? 1; - double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; + + double distance; + + if (obj is IHasPath pathData) + distance = pathData.Path.ExpectedDistance.Value ?? 0; + else + distance = distanceData.Distance; + + distance *= spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime); From 979bbf0d810fb080e883c04c51c53677dd9f01cd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 28 Nov 2023 22:12:23 +0900 Subject: [PATCH 03/59] Wrap echo in double quotes --- .github/workflows/diffcalc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index d4150208d3..5f16e09040 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -189,8 +189,8 @@ jobs: COMMENT_BODY: ${{ github.event.comment.body }} run: | # Add comment environment - echo $COMMENT_BODY | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do - opt=$(echo ${line} | cut -d '=' -f1) + echo "$COMMENT_BODY" | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do + opt=$(echo "${line}" | cut -d '=' -f1) sed -i "s;^${opt}=.*$;${line};" "${{ needs.directory.outputs.GENERATOR_ENV }}" done From 301d503b0b52b4ba7ba16be201efeff7a5040e64 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Nov 2023 16:41:19 +0900 Subject: [PATCH 04/59] Add another source of FP inaccuracy to match osu!stable --- osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 4827ec76aa..2393a248eb 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -190,7 +190,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps else distance = distanceData.Distance; - distance *= spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; + // Do not combine the following two lines! + distance *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; + distance *= spans; TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime); From 1c3bcbd54812f30c1170f2f5a0ab150220505d67 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Nov 2023 17:30:21 +0900 Subject: [PATCH 05/59] Use IHasPath instead of IHasDistance for mania/taiko --- .../Beatmaps/ManiaBeatmapConverter.cs | 4 ++-- ...rator.cs => PathObjectPatternGenerator.cs} | 20 +++++-------------- .../Beatmaps/TaikoBeatmapConverter.cs | 14 ++++--------- 3 files changed, 11 insertions(+), 27 deletions(-) rename osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/{DistanceObjectPatternGenerator.cs => PathObjectPatternGenerator.cs} (96%) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index aaef69f119..ccfe1501bd 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -174,9 +174,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps switch (original) { - case IHasDistance: + case IHasPath: { - var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap); + var generator = new PathObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap); conversion = generator; var positionData = original as IHasPosition; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs similarity index 96% rename from osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs rename to osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs index 8aa128be33..4922915c7d 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PathObjectPatternGenerator.cs @@ -22,13 +22,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// /// A pattern generator for IHasDistance hit objects. /// - internal class DistanceObjectPatternGenerator : PatternGenerator + internal class PathObjectPatternGenerator : PatternGenerator { - /// - /// Base osu! slider scoring distance. - /// - private const float osu_base_scoring_distance = 100; - public readonly int StartTime; public readonly int EndTime; public readonly int SegmentDuration; @@ -36,17 +31,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private PatternType convertType; - public DistanceObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) + public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { convertType = PatternType.None; if (!Beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode) convertType = PatternType.LowProbability; - var distanceData = hitObject as IHasDistance; + var pathData = hitObject as IHasPath; var repeatsData = hitObject as IHasRepeats; - Debug.Assert(distanceData != null); + Debug.Assert(pathData != null); TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); @@ -60,12 +55,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy SpanCount = repeatsData?.SpanCount() ?? 1; StartTime = (int)Math.Round(hitObject.StartTime); - double distance; - - if (hitObject is IHasPath pathData) - distance = pathData.Path.ExpectedDistance.Value ?? 0; - else - distance = distanceData.Distance; + double distance = pathData.Path.ExpectedDistance.Value ?? 0; // This matches stable's calculation. EndTime = (int)Math.Floor(StartTime + distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier); diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 2393a248eb..2551321ff2 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -109,9 +109,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps switch (obj) { - case IHasDistance distanceData: + case IHasPath pathData: { - if (shouldConvertSliderToHits(obj, beatmap, distanceData, out int taikoDuration, out double tickSpacing)) + if (shouldConvertSliderToHits(obj, beatmap, pathData, out int taikoDuration, out double tickSpacing)) { IList> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List>(new[] { samples }); @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps } } - private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDistance distanceData, out int taikoDuration, out double tickSpacing) + private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasPath pathData, out int taikoDuration, out double tickSpacing) { // DO NOT CHANGE OR REFACTOR ANYTHING IN HERE WITHOUT TESTING AGAINST _ALL_ BEATMAPS. // Some of these calculations look redundant, but they are not - extremely small floating point errors are introduced to maintain 1:1 compatibility with stable. @@ -182,13 +182,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps // The true distance, accounting for any repeats. This ends up being the drum roll distance later int spans = (obj as IHasRepeats)?.SpanCount() ?? 1; - - double distance; - - if (obj is IHasPath pathData) - distance = pathData.Path.ExpectedDistance.Value ?? 0; - else - distance = distanceData.Distance; + double distance = pathData.Path.ExpectedDistance.Value ?? 0; // Do not combine the following two lines! distance *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; From 295a1b01d6257980a688551e7aaa0fdd99b49257 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Nov 2023 19:05:24 +0900 Subject: [PATCH 06/59] Adjust catch score grade cutoffs --- .../Scoring/CatchScoreProcessor.cs | 53 +++++++++ .../Visual/Ranking/TestSceneAccuracyCircle.cs | 110 +++++++++++------- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 4 +- .../Expanded/Accuracy/AccuracyCircle.cs | 69 ++++++----- 4 files changed, 160 insertions(+), 76 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 9323296b7f..66c76f9b17 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -4,11 +4,19 @@ using System; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Catch.Scoring { public partial class CatchScoreProcessor : ScoreProcessor { + private const double accuracy_cutoff_x = 1; + private const double accuracy_cutoff_s = 0.98; + private const double accuracy_cutoff_a = 0.94; + private const double accuracy_cutoff_b = 0.9; + private const double accuracy_cutoff_c = 0.85; + private const double accuracy_cutoff_d = 0; + private const int combo_cap = 200; private const double combo_base = 4; @@ -26,5 +34,50 @@ namespace osu.Game.Rulesets.Catch.Scoring protected override double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base)); + + public override ScoreRank RankFromAccuracy(double accuracy) + { + if (accuracy == accuracy_cutoff_x) + return ScoreRank.X; + if (accuracy >= accuracy_cutoff_s) + return ScoreRank.S; + if (accuracy >= accuracy_cutoff_a) + return ScoreRank.A; + if (accuracy >= accuracy_cutoff_b) + return ScoreRank.B; + if (accuracy >= accuracy_cutoff_c) + return ScoreRank.C; + + return ScoreRank.D; + } + + public override double AccuracyCutoffFromRank(ScoreRank rank) + { + switch (rank) + { + case ScoreRank.X: + case ScoreRank.XH: + return accuracy_cutoff_x; + + case ScoreRank.S: + case ScoreRank.SH: + return accuracy_cutoff_s; + + case ScoreRank.A: + return accuracy_cutoff_a; + + case ScoreRank.B: + return accuracy_cutoff_b; + + case ScoreRank.C: + return accuracy_cutoff_c; + + case ScoreRank.D: + return accuracy_cutoff_d; + + default: + throw new ArgumentOutOfRangeException(nameof(rank), rank, null); + } + } } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index 03b168c72c..435dd77120 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -9,6 +9,8 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -22,31 +24,48 @@ namespace osu.Game.Tests.Visual.Ranking { public partial class TestSceneAccuracyCircle : OsuTestScene { - [TestCase(0)] - [TestCase(0.2)] - [TestCase(0.5)] - [TestCase(0.6999)] - [TestCase(0.7)] - [TestCase(0.75)] - [TestCase(0.7999)] - [TestCase(0.8)] - [TestCase(0.85)] - [TestCase(0.8999)] - [TestCase(0.9)] - [TestCase(0.925)] - [TestCase(0.9499)] - [TestCase(0.95)] - [TestCase(0.975)] - [TestCase(0.9999)] - [TestCase(1)] - public void TestRank(double accuracy) + [Test] + public void TestOsuRank() { - var score = createScore(accuracy, ScoreProcessor.RankFromAccuracy(accuracy)); - - addCircleStep(score); + addCircleStep(createScore(0, new OsuRuleset())); + addCircleStep(createScore(0.5, new OsuRuleset())); + addCircleStep(createScore(0.699, new OsuRuleset())); + addCircleStep(createScore(0.7, new OsuRuleset())); + addCircleStep(createScore(0.75, new OsuRuleset())); + addCircleStep(createScore(0.799, new OsuRuleset())); + addCircleStep(createScore(0.8, new OsuRuleset())); + addCircleStep(createScore(0.85, new OsuRuleset())); + addCircleStep(createScore(0.899, new OsuRuleset())); + addCircleStep(createScore(0.9, new OsuRuleset())); + addCircleStep(createScore(0.925, new OsuRuleset())); + addCircleStep(createScore(0.9499, new OsuRuleset())); + addCircleStep(createScore(0.95, new OsuRuleset())); + addCircleStep(createScore(0.975, new OsuRuleset())); + addCircleStep(createScore(0.99, new OsuRuleset())); + addCircleStep(createScore(1, new OsuRuleset())); } - private void addCircleStep(ScoreInfo score) => AddStep("add panel", () => + [Test] + public void TestCatchRank() + { + addCircleStep(createScore(0, new CatchRuleset())); + addCircleStep(createScore(0.5, new CatchRuleset())); + addCircleStep(createScore(0.8499, new CatchRuleset())); + addCircleStep(createScore(0.85, new CatchRuleset())); + addCircleStep(createScore(0.875, new CatchRuleset())); + addCircleStep(createScore(0.899, new CatchRuleset())); + addCircleStep(createScore(0.9, new CatchRuleset())); + addCircleStep(createScore(0.925, new CatchRuleset())); + addCircleStep(createScore(0.9399, new CatchRuleset())); + addCircleStep(createScore(0.94, new CatchRuleset())); + addCircleStep(createScore(0.9675, new CatchRuleset())); + addCircleStep(createScore(0.9799, new CatchRuleset())); + addCircleStep(createScore(0.98, new CatchRuleset())); + addCircleStep(createScore(0.99, new CatchRuleset())); + addCircleStep(createScore(1, new CatchRuleset())); + } + + private void addCircleStep(ScoreInfo score) => AddStep($"add panel ({score.DisplayAccuracy})", () => { Children = new Drawable[] { @@ -73,28 +92,33 @@ namespace osu.Game.Tests.Visual.Ranking }; }); - private ScoreInfo createScore(double accuracy, ScoreRank rank) => new ScoreInfo + private ScoreInfo createScore(double accuracy, Ruleset ruleset) { - User = new APIUser + var scoreProcessor = ruleset.CreateScoreProcessor(); + + return new ScoreInfo { - Id = 2, - Username = "peppy", - }, - BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, - Ruleset = new OsuRuleset().RulesetInfo, - Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, - TotalScore = 2845370, - Accuracy = accuracy, - MaxCombo = 999, - Rank = rank, - Date = DateTimeOffset.Now, - Statistics = - { - { HitResult.Miss, 1 }, - { HitResult.Meh, 50 }, - { HitResult.Good, 100 }, - { HitResult.Great, 300 }, - } - }; + User = new APIUser + { + Id = 2, + Username = "peppy", + }, + BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + Ruleset = ruleset.RulesetInfo, + Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, + TotalScore = 2845370, + Accuracy = accuracy, + MaxCombo = 999, + Rank = scoreProcessor.RankFromAccuracy(accuracy), + Date = DateTimeOffset.Now, + Statistics = + { + { HitResult.Miss, 1 }, + { HitResult.Meh, 50 }, + { HitResult.Good, 100 }, + { HitResult.Great, 300 }, + } + }; + } } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 4e899479bd..92336b2c21 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -446,7 +446,7 @@ namespace osu.Game.Rulesets.Scoring /// /// Given an accuracy (0..1), return the correct . /// - public static ScoreRank RankFromAccuracy(double accuracy) + public virtual ScoreRank RankFromAccuracy(double accuracy) { if (accuracy == accuracy_cutoff_x) return ScoreRank.X; @@ -466,7 +466,7 @@ namespace osu.Game.Rulesets.Scoring /// Given a , return the cutoff accuracy (0..1). /// Accuracy must be greater than or equal to the cutoff to qualify for the provided rank. /// - public static double AccuracyCutoffFromRank(ScoreRank rank) + public virtual double AccuracyCutoffFromRank(ScoreRank rank) { switch (rank) { diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 2ec4270c3c..80ff872312 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -29,13 +29,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy /// public partial class AccuracyCircle : CompositeDrawable { - private static readonly double accuracy_x = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.X); - private static readonly double accuracy_s = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.S); - private static readonly double accuracy_a = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.A); - private static readonly double accuracy_b = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.B); - private static readonly double accuracy_c = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.C); - private static readonly double accuracy_d = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.D); - /// /// Duration for the transforms causing this component to appear. /// @@ -110,12 +103,26 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private double lastTickPlaybackTime; private bool isTicking; + private readonly double accuracyX; + private readonly double accuracyS; + private readonly double accuracyA; + private readonly double accuracyB; + private readonly double accuracyC; + private readonly double accuracyD; private readonly bool withFlair; public AccuracyCircle(ScoreInfo score, bool withFlair = false) { this.score = score; this.withFlair = withFlair; + + ScoreProcessor scoreProcessor = score.Ruleset.CreateInstance().CreateScoreProcessor(); + accuracyX = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.X); + accuracyS = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.S); + accuracyA = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.A); + accuracyB = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.B); + accuracyC = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.C); + accuracyD = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.D); } [BackgroundDependencyLoader] @@ -158,49 +165,49 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.X), InnerRadius = RANK_CIRCLE_RADIUS, - Current = { Value = accuracy_x } + Current = { Value = accuracyX } }, new CircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.S), InnerRadius = RANK_CIRCLE_RADIUS, - Current = { Value = accuracy_x - virtual_ss_percentage } + Current = { Value = accuracyX - virtual_ss_percentage } }, new CircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.A), InnerRadius = RANK_CIRCLE_RADIUS, - Current = { Value = accuracy_s } + Current = { Value = accuracyS } }, new CircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.B), InnerRadius = RANK_CIRCLE_RADIUS, - Current = { Value = accuracy_a } + Current = { Value = accuracyA } }, new CircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.C), InnerRadius = RANK_CIRCLE_RADIUS, - Current = { Value = accuracy_b } + Current = { Value = accuracyB } }, new CircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.D), InnerRadius = RANK_CIRCLE_RADIUS, - Current = { Value = accuracy_c } + Current = { Value = accuracyC } }, - new RankNotch((float)accuracy_x), - new RankNotch((float)(accuracy_x - virtual_ss_percentage)), - new RankNotch((float)accuracy_s), - new RankNotch((float)accuracy_a), - new RankNotch((float)accuracy_b), - new RankNotch((float)accuracy_c), + new RankNotch((float)accuracyX), + new RankNotch((float)(accuracyX - virtual_ss_percentage)), + new RankNotch((float)accuracyS), + new RankNotch((float)accuracyA), + new RankNotch((float)accuracyB), + new RankNotch((float)accuracyC), new BufferedContainer { Name = "Graded circle mask", @@ -229,12 +236,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Children = new[] { // The S and A badges are moved down slightly to prevent collision with the SS badge. - new RankBadge(accuracy_x, accuracy_x, getRank(ScoreRank.X)), - new RankBadge(accuracy_s, Interpolation.Lerp(accuracy_s, (accuracy_x - virtual_ss_percentage), 0.25), getRank(ScoreRank.S)), - new RankBadge(accuracy_a, Interpolation.Lerp(accuracy_a, accuracy_s, 0.25), getRank(ScoreRank.A)), - new RankBadge(accuracy_b, Interpolation.Lerp(accuracy_b, accuracy_a, 0.5), getRank(ScoreRank.B)), - new RankBadge(accuracy_c, Interpolation.Lerp(accuracy_c, accuracy_b, 0.5), getRank(ScoreRank.C)), - new RankBadge(accuracy_d, Interpolation.Lerp(accuracy_d, accuracy_c, 0.5), getRank(ScoreRank.D)), + new RankBadge(accuracyX, accuracyX, getRank(ScoreRank.X)), + new RankBadge(accuracyS, Interpolation.Lerp(accuracyS, (accuracyX - virtual_ss_percentage), 0.25), getRank(ScoreRank.S)), + new RankBadge(accuracyA, Interpolation.Lerp(accuracyA, accuracyS, 0.25), getRank(ScoreRank.A)), + new RankBadge(accuracyB, Interpolation.Lerp(accuracyB, accuracyA, 0.5), getRank(ScoreRank.B)), + new RankBadge(accuracyC, Interpolation.Lerp(accuracyC, accuracyB, 0.5), getRank(ScoreRank.C)), + new RankBadge(accuracyD, Interpolation.Lerp(accuracyD, accuracyC, 0.5), getRank(ScoreRank.D)), } }, rankText = new RankText(score.Rank) @@ -280,10 +287,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy double targetAccuracy = score.Accuracy; double[] notchPercentages = { - accuracy_s, - accuracy_a, - accuracy_b, - accuracy_c, + accuracyS, + accuracyA, + accuracyB, + accuracyC, }; // Ensure the gauge overshoots or undershoots a bit so it doesn't land in the gaps of the inner graded circle (caused by `RankNotch`es), @@ -302,7 +309,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy if (score.Rank == ScoreRank.X || score.Rank == ScoreRank.XH) targetAccuracy = 1; else - targetAccuracy = Math.Min(accuracy_x - virtual_ss_percentage - NOTCH_WIDTH_PERCENTAGE / 2, targetAccuracy); + targetAccuracy = Math.Min(accuracyX - virtual_ss_percentage - NOTCH_WIDTH_PERCENTAGE / 2, targetAccuracy); // The accuracy circle gauge visually fills up a bit too much. // This wouldn't normally matter but we want it to align properly with the inner graded circle in the above cases. @@ -339,7 +346,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy if (badge.Accuracy > score.Accuracy) continue; - using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(accuracy_x - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION)) + using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(accuracyX - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION)) { badge.Appear(); From 1cfcaee121c7f5b62610a751d1251b752c7dc794 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Nov 2023 20:29:50 +0900 Subject: [PATCH 07/59] Reorder badges so that SS shows above others This isn't perfect and probably needs much more consideration, but let's at least give the "better" ranks more visibility by bringing them to the front. Of note, this is only important due to the changes to osu!catch accuracy-grade cutoffs, which brings things closer in proximity than ever before. --- .../Ranking/Expanded/Accuracy/AccuracyCircle.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 80ff872312..8cbca74466 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -235,13 +235,13 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Padding = new MarginPadding { Vertical = -15, Horizontal = -20 }, Children = new[] { - // The S and A badges are moved down slightly to prevent collision with the SS badge. - new RankBadge(accuracyX, accuracyX, getRank(ScoreRank.X)), - new RankBadge(accuracyS, Interpolation.Lerp(accuracyS, (accuracyX - virtual_ss_percentage), 0.25), getRank(ScoreRank.S)), - new RankBadge(accuracyA, Interpolation.Lerp(accuracyA, accuracyS, 0.25), getRank(ScoreRank.A)), - new RankBadge(accuracyB, Interpolation.Lerp(accuracyB, accuracyA, 0.5), getRank(ScoreRank.B)), - new RankBadge(accuracyC, Interpolation.Lerp(accuracyC, accuracyB, 0.5), getRank(ScoreRank.C)), new RankBadge(accuracyD, Interpolation.Lerp(accuracyD, accuracyC, 0.5), getRank(ScoreRank.D)), + new RankBadge(accuracyC, Interpolation.Lerp(accuracyC, accuracyB, 0.5), getRank(ScoreRank.C)), + new RankBadge(accuracyB, Interpolation.Lerp(accuracyB, accuracyA, 0.5), getRank(ScoreRank.B)), + // The S and A badges are moved down slightly to prevent collision with the SS badge. + new RankBadge(accuracyA, Interpolation.Lerp(accuracyA, accuracyS, 0.25), getRank(ScoreRank.A)), + new RankBadge(accuracyS, Interpolation.Lerp(accuracyS, (accuracyX - virtual_ss_percentage), 0.25), getRank(ScoreRank.S)), + new RankBadge(accuracyX, accuracyX, getRank(ScoreRank.X)), } }, rankText = new RankText(score.Rank) From 3553717cc6238251cd7a0dd95d4749584c23504c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Nov 2023 21:28:25 +0900 Subject: [PATCH 08/59] Fix results screen not including slider end misses in tick count --- osu.Game/Scoring/ScoreInfo.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index d712702331..5545ba552e 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -342,23 +342,7 @@ namespace osu.Game.Scoring switch (r.result) { case HitResult.SmallTickHit: - { - int total = value + Statistics.GetValueOrDefault(HitResult.SmallTickMiss); - if (total > 0) - yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); - - break; - } - case HitResult.LargeTickHit: - { - int total = value + Statistics.GetValueOrDefault(HitResult.LargeTickMiss); - if (total > 0) - yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); - - break; - } - case HitResult.LargeBonus: case HitResult.SmallBonus: if (MaximumStatistics.TryGetValue(r.result, out int count) && count > 0) From 831fe5b9f2a833dc8d1dbd4c4340376c10c854cc Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 30 Nov 2023 14:36:23 +0100 Subject: [PATCH 09/59] Use collection assert to ease debugging --- .../Visual/Online/TestSceneChatLink.cs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 7616b9b83c..70eca64084 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -107,27 +107,11 @@ namespace osu.Game.Tests.Visual.Online textContainer.Add(newLine); }); - AddAssert($"msg #{index} has {linkAmount} link(s)", () => newLine.Message.Links.Count == linkAmount); - AddAssert($"msg #{index} has the right action", hasExpectedActions); + AddAssert($"msg #{index} has {linkAmount} link(s)", () => newLine.Message.Links, () => Has.Count.EqualTo(linkAmount)); + AddAssert($"msg #{index} has the right action", () => newLine.Message.Links.Select(l => l.Action), () => Is.EqualTo(expectedActions)); //AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); AddAssert($"msg #{index} shows {linkAmount} link(s)", isShowingLinks); - bool hasExpectedActions() - { - var expectedActionsList = expectedActions.ToList(); - - if (expectedActionsList.Count != newLine.Message.Links.Count) - return false; - - for (int i = 0; i < newLine.Message.Links.Count; i++) - { - var action = newLine.Message.Links[i].Action; - if (action != expectedActions[i]) return false; - } - - return true; - } - //bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font.Italics); bool isShowingLinks() From 9a32c0368e6d03153ac21070cf5a979e200df674 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 30 Nov 2023 14:38:58 +0100 Subject: [PATCH 10/59] Remove redundant `linkAmount` parameter --- .../Visual/Online/TestSceneChatLink.cs | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 70eca64084..1ab7e3257f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -63,40 +63,40 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks("test!"); addMessageWithChecks("dev.ppy.sh!"); - addMessageWithChecks("https://dev.ppy.sh!", 1, expectedActions: LinkAction.External); - addMessageWithChecks("http://dev.ppy.sh!", 1, expectedActions: LinkAction.External); - addMessageWithChecks("forgothttps://dev.ppy.sh!", 1, expectedActions: LinkAction.External); - addMessageWithChecks("forgothttp://dev.ppy.sh!", 1, expectedActions: LinkAction.External); - addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp); - addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.OpenWiki); - addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[https://dev.ppy.sh/home New site] (new link format)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[osu forums](https://dev.ppy.sh/forum) (new link format 2)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[https://dev.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", 1, expectedActions: LinkAction.External); - addMessageWithChecks("is now listening to [https://dev.ppy.sh/s/93523 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmapSet); - addMessageWithChecks("is now playing [https://dev.ppy.sh/b/252238 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmap); - addMessageWithChecks("Let's (try)[https://dev.ppy.sh/home] [https://dev.ppy.sh/b/252238 multiple links] https://dev.ppy.sh/home", 3, + addMessageWithChecks("https://dev.ppy.sh!", expectedActions: LinkAction.External); + addMessageWithChecks("http://dev.ppy.sh!", expectedActions: LinkAction.External); + addMessageWithChecks("forgothttps://dev.ppy.sh!", expectedActions: LinkAction.External); + addMessageWithChecks("forgothttp://dev.ppy.sh!", expectedActions: LinkAction.External); + addMessageWithChecks("00:12:345 (1,2) - Test?", expectedActions: LinkAction.OpenEditorTimestamp); + addMessageWithChecks("Wiki link for tasty [[Performance Points]]", expectedActions: LinkAction.OpenWiki); + addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", expectedActions: LinkAction.External); + addMessageWithChecks("[https://dev.ppy.sh/home New site] (new link format)", expectedActions: LinkAction.External); + addMessageWithChecks("[osu forums](https://dev.ppy.sh/forum) (new link format 2)", expectedActions: LinkAction.External); + addMessageWithChecks("[https://dev.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", expectedActions: LinkAction.External); + addMessageWithChecks("is now listening to [https://dev.ppy.sh/s/93523 IMAGE -MATERIAL- ]", isAction: true, expectedActions: LinkAction.OpenBeatmapSet); + addMessageWithChecks("is now playing [https://dev.ppy.sh/b/252238 IMAGE -MATERIAL- ]", isAction: true, expectedActions: LinkAction.OpenBeatmap); + addMessageWithChecks("Let's (try)[https://dev.ppy.sh/home] [https://dev.ppy.sh/b/252238 multiple links] https://dev.ppy.sh/home", expectedActions: new[] { LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External }); - addMessageWithChecks("[https://dev.ppy.sh/home New link format with escaped [and \\[ paired] braces]", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", 2, + addMessageWithChecks("[https://dev.ppy.sh/home New link format with escaped [and \\[ paired] braces]", expectedActions: LinkAction.External); + addMessageWithChecks("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", expectedActions: LinkAction.External); + addMessageWithChecks("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", expectedActions: new[] { LinkAction.External, LinkAction.OpenWiki }); // note that there's 0 links here (they get removed if a channel is not found) addMessageWithChecks("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present)."); - addMessageWithChecks("I am important!", 0, false, true); - addMessageWithChecks("feels important", 0, true, true); - addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External); - addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch); - addMessageWithChecks("Join my multiplayer gameosump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch); - addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch); - addMessageWithChecks($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", 1, expectedActions: LinkAction.OpenChannel); - addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel); - addMessageWithChecks($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel); - addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel }); - addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel); + addMessageWithChecks("I am important!", isAction: false, isImportant: true); + addMessageWithChecks("feels important", isAction: true, isImportant: true); + addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", isAction: true, isImportant: true, expectedActions: LinkAction.External); + addMessageWithChecks("Join my multiplayer game osump://12346.", expectedActions: LinkAction.JoinMultiplayerMatch); + addMessageWithChecks("Join my multiplayer gameosump://12346.", expectedActions: LinkAction.JoinMultiplayerMatch); + addMessageWithChecks("Join my [multiplayer game](osump://12346).", expectedActions: LinkAction.JoinMultiplayerMatch); + addMessageWithChecks($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", expectedActions: LinkAction.OpenChannel); + addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", expectedActions: LinkAction.OpenChannel); + addMessageWithChecks($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", expectedActions: LinkAction.OpenChannel); + addMessageWithChecks("Join my #english or #japanese channels.", expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel }); + addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", expectedActions: LinkAction.OpenChannel); addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20"); - void addMessageWithChecks(string text, int linkAmount = 0, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions) + void addMessageWithChecks(string text, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions) { ChatLine newLine = null; int index = messageIndex++; @@ -107,10 +107,9 @@ namespace osu.Game.Tests.Visual.Online textContainer.Add(newLine); }); - AddAssert($"msg #{index} has {linkAmount} link(s)", () => newLine.Message.Links, () => Has.Count.EqualTo(linkAmount)); AddAssert($"msg #{index} has the right action", () => newLine.Message.Links.Select(l => l.Action), () => Is.EqualTo(expectedActions)); //AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); - AddAssert($"msg #{index} shows {linkAmount} link(s)", isShowingLinks); + AddAssert($"msg #{index} shows {expectedActions.Length} link(s)", isShowingLinks); //bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font.Italics); From d2324cd8f9ca27da010d26d542ccd80d7a3a29d8 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 30 Nov 2023 10:20:01 -0800 Subject: [PATCH 11/59] Fix chat overlay top bar icon being incorrect --- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 84fd342493..4fc9fbb6d5 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Chat { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Icon = HexaconsIcons.Social, + Icon = HexaconsIcons.Messaging, Size = new Vector2(24), }, // Placeholder text From 3ca3f254925603f953163ad9f7d694b18865cd13 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 30 Nov 2023 23:25:28 +0100 Subject: [PATCH 12/59] Extract local method --- .../Visual/Online/TestSceneChatLink.cs | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 1ab7e3257f..a0fcdf4337 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -59,8 +59,6 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestLinksGeneral() { - int messageIndex = 0; - addMessageWithChecks("test!"); addMessageWithChecks("dev.ppy.sh!"); addMessageWithChecks("https://dev.ppy.sh!", expectedActions: LinkAction.External); @@ -95,36 +93,35 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks("Join my #english or #japanese channels.", expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel }); addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", expectedActions: LinkAction.OpenChannel); addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20"); + } - void addMessageWithChecks(string text, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions) + private void addMessageWithChecks(string text, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions) + { + ChatLine newLine = null; + + AddStep("add message", () => { - ChatLine newLine = null; - int index = messageIndex++; + newLine = new ChatLine(new DummyMessage(text, isAction, isImportant)); + textContainer.Add(newLine); + }); - AddStep("add message", () => - { - newLine = new ChatLine(new DummyMessage(text, isAction, isImportant, index)); - textContainer.Add(newLine); - }); + AddAssert($"msg has the right action", () => newLine.Message.Links.Select(l => l.Action), () => Is.EqualTo(expectedActions)); + //AddAssert($"msg is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); + AddAssert($"msg shows {expectedActions.Length} link(s)", isShowingLinks); - AddAssert($"msg #{index} has the right action", () => newLine.Message.Links.Select(l => l.Action), () => Is.EqualTo(expectedActions)); - //AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); - AddAssert($"msg #{index} shows {expectedActions.Length} link(s)", isShowingLinks); + //bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font.Italics); - //bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font.Italics); + bool isShowingLinks() + { + bool hasBackground = !string.IsNullOrEmpty(newLine.Message.Sender.Colour); - bool isShowingLinks() - { - bool hasBackground = !string.IsNullOrEmpty(newLine.Message.Sender.Colour); + Color4 textColour = isAction && hasBackground ? Color4Extensions.FromHex(newLine.Message.Sender.Colour) : Color4.White; - Color4 textColour = isAction && hasBackground ? Color4Extensions.FromHex(newLine.Message.Sender.Colour) : Color4.White; + var linkCompilers = newLine.DrawableContentFlow.Where(d => d is DrawableLinkCompiler).ToList(); + var linkSprites = linkCompilers.SelectMany(comp => ((DrawableLinkCompiler)comp).Parts); - var linkCompilers = newLine.DrawableContentFlow.Where(d => d is DrawableLinkCompiler).ToList(); - var linkSprites = linkCompilers.SelectMany(comp => ((DrawableLinkCompiler)comp).Parts); - - return linkSprites.All(d => d.Colour == linkColour) - && newLine.DrawableContentFlow.Except(linkSprites.Concat(linkCompilers)).All(d => d.Colour == textColour); - } + return linkSprites.All(d => d.Colour == linkColour) + && newLine.DrawableContentFlow.Except(linkSprites.Concat(linkCompilers)).All(d => d.Colour == textColour); } } From 52ffbcc6db7e21eca7f49ac6d124b8b01c12ed4d Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 30 Nov 2023 23:29:22 +0100 Subject: [PATCH 13/59] Move special cases to `TestCase`'d test --- osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index a0fcdf4337..52eebeb9ce 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -71,8 +71,6 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks("[https://dev.ppy.sh/home New site] (new link format)", expectedActions: LinkAction.External); addMessageWithChecks("[osu forums](https://dev.ppy.sh/forum) (new link format 2)", expectedActions: LinkAction.External); addMessageWithChecks("[https://dev.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", expectedActions: LinkAction.External); - addMessageWithChecks("is now listening to [https://dev.ppy.sh/s/93523 IMAGE -MATERIAL- ]", isAction: true, expectedActions: LinkAction.OpenBeatmapSet); - addMessageWithChecks("is now playing [https://dev.ppy.sh/b/252238 IMAGE -MATERIAL- ]", isAction: true, expectedActions: LinkAction.OpenBeatmap); addMessageWithChecks("Let's (try)[https://dev.ppy.sh/home] [https://dev.ppy.sh/b/252238 multiple links] https://dev.ppy.sh/home", expectedActions: new[] { LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External }); addMessageWithChecks("[https://dev.ppy.sh/home New link format with escaped [and \\[ paired] braces]", expectedActions: LinkAction.External); @@ -81,9 +79,6 @@ namespace osu.Game.Tests.Visual.Online expectedActions: new[] { LinkAction.External, LinkAction.OpenWiki }); // note that there's 0 links here (they get removed if a channel is not found) addMessageWithChecks("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present)."); - addMessageWithChecks("I am important!", isAction: false, isImportant: true); - addMessageWithChecks("feels important", isAction: true, isImportant: true); - addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", isAction: true, isImportant: true, expectedActions: LinkAction.External); addMessageWithChecks("Join my multiplayer game osump://12346.", expectedActions: LinkAction.JoinMultiplayerMatch); addMessageWithChecks("Join my multiplayer gameosump://12346.", expectedActions: LinkAction.JoinMultiplayerMatch); addMessageWithChecks("Join my [multiplayer game](osump://12346).", expectedActions: LinkAction.JoinMultiplayerMatch); @@ -95,6 +90,16 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20"); } + [TestCase("is now listening to [https://dev.ppy.sh/s/93523 IMAGE -MATERIAL- ]", true, false, LinkAction.OpenBeatmapSet)] + [TestCase("is now playing [https://dev.ppy.sh/b/252238 IMAGE -MATERIAL- ]", true, false, LinkAction.OpenBeatmap)] + [TestCase("I am important!", false, true)] + [TestCase("feels important", true, true)] + [TestCase("likes to post this [https://dev.ppy.sh/home link].", true, true, LinkAction.External)] + public void TestActionAndImportantLinks(string text, bool isAction, bool isImportant, params LinkAction[] expectedActions) + { + addMessageWithChecks(text, isAction, isImportant, expectedActions); + } + private void addMessageWithChecks(string text, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions) { ChatLine newLine = null; From 0520ac265fa57bbbe7f07dd9e63a966f332414d8 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 30 Nov 2023 23:34:43 +0100 Subject: [PATCH 14/59] Move to `TestCase`-based test --- .../Visual/Online/TestSceneChatLink.cs | 59 +++++++++---------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 52eebeb9ce..ae94d1887e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -56,38 +56,35 @@ namespace osu.Game.Tests.Visual.Online textContainer.Clear(); }); - [Test] - public void TestLinksGeneral() + [TestCase("test!")] + [TestCase("dev.ppy.sh!")] + [TestCase("https://dev.ppy.sh!", LinkAction.External)] + [TestCase("http://dev.ppy.sh!", LinkAction.External)] + [TestCase("forgothttps://dev.ppy.sh!", LinkAction.External)] + [TestCase("forgothttp://dev.ppy.sh!", LinkAction.External)] + [TestCase("00:12:345 (1,2) - Test?", LinkAction.OpenEditorTimestamp)] + [TestCase("Wiki link for tasty [[Performance Points]]", LinkAction.OpenWiki)] + [TestCase("(osu forums)[https://dev.ppy.sh/forum] (old link format)", LinkAction.External)] + [TestCase("[https://dev.ppy.sh/home New site] (new link format)", LinkAction.External)] + [TestCase("[osu forums](https://dev.ppy.sh/forum) (new link format 2)", LinkAction.External)] + [TestCase("[https://dev.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", LinkAction.External)] + [TestCase("Let's (try)[https://dev.ppy.sh/home] [https://dev.ppy.sh/b/252238 multiple links] https://dev.ppy.sh/home", LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External)] + [TestCase("[https://dev.ppy.sh/home New link format with escaped [and \\[ paired] braces]", LinkAction.External)] + [TestCase("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", LinkAction.External)] + [TestCase("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", LinkAction.External, LinkAction.OpenWiki)] + [TestCase("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present).")] // note that there's 0 links here (they get removed if a channel is not found) + [TestCase("Join my multiplayer game osump://12346.", LinkAction.JoinMultiplayerMatch)] + [TestCase("Join my multiplayer gameosump://12346.", LinkAction.JoinMultiplayerMatch)] + [TestCase("Join my [multiplayer game](osump://12346).", LinkAction.JoinMultiplayerMatch)] + [TestCase($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", LinkAction.OpenChannel)] + [TestCase($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)] + [TestCase($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", LinkAction.OpenChannel)] + [TestCase("Join my #english or #japanese channels.", LinkAction.OpenChannel, LinkAction.OpenChannel)] + [TestCase("Join my #english or #nonexistent #hashtag channels.", LinkAction.OpenChannel)] + [TestCase("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20")] + public void TestLinksGeneral(string text, params LinkAction[] actions) { - addMessageWithChecks("test!"); - addMessageWithChecks("dev.ppy.sh!"); - addMessageWithChecks("https://dev.ppy.sh!", expectedActions: LinkAction.External); - addMessageWithChecks("http://dev.ppy.sh!", expectedActions: LinkAction.External); - addMessageWithChecks("forgothttps://dev.ppy.sh!", expectedActions: LinkAction.External); - addMessageWithChecks("forgothttp://dev.ppy.sh!", expectedActions: LinkAction.External); - addMessageWithChecks("00:12:345 (1,2) - Test?", expectedActions: LinkAction.OpenEditorTimestamp); - addMessageWithChecks("Wiki link for tasty [[Performance Points]]", expectedActions: LinkAction.OpenWiki); - addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", expectedActions: LinkAction.External); - addMessageWithChecks("[https://dev.ppy.sh/home New site] (new link format)", expectedActions: LinkAction.External); - addMessageWithChecks("[osu forums](https://dev.ppy.sh/forum) (new link format 2)", expectedActions: LinkAction.External); - addMessageWithChecks("[https://dev.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", expectedActions: LinkAction.External); - addMessageWithChecks("Let's (try)[https://dev.ppy.sh/home] [https://dev.ppy.sh/b/252238 multiple links] https://dev.ppy.sh/home", - expectedActions: new[] { LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External }); - addMessageWithChecks("[https://dev.ppy.sh/home New link format with escaped [and \\[ paired] braces]", expectedActions: LinkAction.External); - addMessageWithChecks("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", expectedActions: LinkAction.External); - addMessageWithChecks("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", - expectedActions: new[] { LinkAction.External, LinkAction.OpenWiki }); - // note that there's 0 links here (they get removed if a channel is not found) - addMessageWithChecks("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present)."); - addMessageWithChecks("Join my multiplayer game osump://12346.", expectedActions: LinkAction.JoinMultiplayerMatch); - addMessageWithChecks("Join my multiplayer gameosump://12346.", expectedActions: LinkAction.JoinMultiplayerMatch); - addMessageWithChecks("Join my [multiplayer game](osump://12346).", expectedActions: LinkAction.JoinMultiplayerMatch); - addMessageWithChecks($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", expectedActions: LinkAction.OpenChannel); - addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", expectedActions: LinkAction.OpenChannel); - addMessageWithChecks($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", expectedActions: LinkAction.OpenChannel); - addMessageWithChecks("Join my #english or #japanese channels.", expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel }); - addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", expectedActions: LinkAction.OpenChannel); - addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20"); + addMessageWithChecks(text, expectedActions: actions); } [TestCase("is now listening to [https://dev.ppy.sh/s/93523 IMAGE -MATERIAL- ]", true, false, LinkAction.OpenBeatmapSet)] From 7b1ccb38cb8070cdcf576b4e14c9437589f74867 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 30 Nov 2023 23:35:10 +0100 Subject: [PATCH 15/59] Remove redundant string interpolation --- osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index ae94d1887e..f4dd4289c3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -107,8 +107,8 @@ namespace osu.Game.Tests.Visual.Online textContainer.Add(newLine); }); - AddAssert($"msg has the right action", () => newLine.Message.Links.Select(l => l.Action), () => Is.EqualTo(expectedActions)); - //AddAssert($"msg is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); + AddAssert("msg has the right action", () => newLine.Message.Links.Select(l => l.Action), () => Is.EqualTo(expectedActions)); + //AddAssert("msg is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); AddAssert($"msg shows {expectedActions.Length} link(s)", isShowingLinks); //bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font.Italics); From 21fedff54fbc675947022b61138ca49b90e49f21 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 1 Dec 2023 00:29:37 +0100 Subject: [PATCH 16/59] Check number of links shown --- osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index f4dd4289c3..5947c01b96 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -123,7 +123,8 @@ namespace osu.Game.Tests.Visual.Online var linkSprites = linkCompilers.SelectMany(comp => ((DrawableLinkCompiler)comp).Parts); return linkSprites.All(d => d.Colour == linkColour) - && newLine.DrawableContentFlow.Except(linkSprites.Concat(linkCompilers)).All(d => d.Colour == textColour); + && newLine.DrawableContentFlow.Except(linkSprites.Concat(linkCompilers)).All(d => d.Colour == textColour) + && linkCompilers.Count == expectedActions.Length; } } From fba6349c6528793328d3125222b0328f253764fa Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 1 Dec 2023 00:36:40 +0100 Subject: [PATCH 17/59] Remove unused properties --- osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 5947c01b96..b9a65f734f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -167,21 +167,12 @@ namespace osu.Game.Tests.Visual.Online { private static long messageCounter; - internal static readonly APIUser TEST_SENDER_BACKGROUND = new APIUser - { - Username = @"i-am-important", - Id = 42, - Colour = "#250cc9", - }; - internal static readonly APIUser TEST_SENDER = new APIUser { Username = @"Somebody", Id = 1, }; - public new DateTimeOffset Timestamp = DateTimeOffset.Now; - public DummyMessage(string text, bool isAction = false, bool isImportant = false, int number = 0) : base(messageCounter++) { From 1a33bfbb3a8d0b298651844c4b9ea4bbff141629 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 1 Dec 2023 00:37:16 +0100 Subject: [PATCH 18/59] Enable NRT --- osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index b9a65f734f..8d4ff82303 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.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.Linq; using NUnit.Framework; @@ -99,7 +97,7 @@ namespace osu.Game.Tests.Visual.Online private void addMessageWithChecks(string text, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions) { - ChatLine newLine = null; + ChatLine newLine = null!; AddStep("add message", () => { @@ -138,7 +136,7 @@ namespace osu.Game.Tests.Visual.Online addEchoWithWait("[https://dev.ppy.sh/forum let's try multiple words too!]"); addEchoWithWait("(long loading times! clickable while loading?)[https://dev.ppy.sh/home]", null, 5000); - void addEchoWithWait(string text, string completeText = null, double delay = 250) + void addEchoWithWait(string text, string? completeText = null, double delay = 250) { int index = messageIndex++; From 7f9ae55f5eab245e2051d0a85744031289424ad4 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 1 Dec 2023 00:45:35 +0100 Subject: [PATCH 19/59] Add passing tests --- osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 8d4ff82303..2dde1447e4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -60,7 +60,9 @@ namespace osu.Game.Tests.Visual.Online [TestCase("http://dev.ppy.sh!", LinkAction.External)] [TestCase("forgothttps://dev.ppy.sh!", LinkAction.External)] [TestCase("forgothttp://dev.ppy.sh!", LinkAction.External)] + [TestCase("00:12:345 - Test?", LinkAction.OpenEditorTimestamp)] [TestCase("00:12:345 (1,2) - Test?", LinkAction.OpenEditorTimestamp)] + [TestCase($"{OsuGameBase.OSU_PROTOCOL}edit/00:12:345 - Test?", LinkAction.OpenEditorTimestamp)] [TestCase("Wiki link for tasty [[Performance Points]]", LinkAction.OpenWiki)] [TestCase("(osu forums)[https://dev.ppy.sh/forum] (old link format)", LinkAction.External)] [TestCase("[https://dev.ppy.sh/home New site] (new link format)", LinkAction.External)] From c395ae2460fbbc7e8329060732fa132f1df7ca01 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 1 Dec 2023 00:49:21 +0100 Subject: [PATCH 20/59] Add failing tests They throws `ArgumentOutOfRangeException` on the first drawable Update() --- osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 2dde1447e4..e77ff5c1cd 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -63,6 +63,8 @@ namespace osu.Game.Tests.Visual.Online [TestCase("00:12:345 - Test?", LinkAction.OpenEditorTimestamp)] [TestCase("00:12:345 (1,2) - Test?", LinkAction.OpenEditorTimestamp)] [TestCase($"{OsuGameBase.OSU_PROTOCOL}edit/00:12:345 - Test?", LinkAction.OpenEditorTimestamp)] + [TestCase($"{OsuGameBase.OSU_PROTOCOL}edit/00:12:345 (1,2) - Test?", LinkAction.OpenEditorTimestamp)] + [TestCase($"{OsuGameBase.OSU_PROTOCOL}00:12:345 - not an editor timestamp", LinkAction.External)] [TestCase("Wiki link for tasty [[Performance Points]]", LinkAction.OpenWiki)] [TestCase("(osu forums)[https://dev.ppy.sh/forum] (old link format)", LinkAction.External)] [TestCase("[https://dev.ppy.sh/home New site] (new link format)", LinkAction.External)] From 152c7e513ea98e0e25f3e3461c81307b6bd61376 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 30 Nov 2023 22:59:02 +0100 Subject: [PATCH 21/59] Ignore overlapping links instead of crashing --- osu.Game/Graphics/Containers/LinkFlowContainer.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index 40e883f8ac..aa72996fff 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Online; using osu.Game.Users; @@ -47,9 +48,16 @@ namespace osu.Game.Graphics.Containers foreach (var link in links) { + string displayText = text.Substring(link.Index, link.Length); + + if (previousLinkEnd > link.Index) + { + Logger.Log($@"Link ""{link.Url}"" with text ""{displayText}"" overlaps previous link, ignoring."); + continue; + } + AddText(text[previousLinkEnd..link.Index]); - string displayText = text.Substring(link.Index, link.Length); object linkArgument = link.Argument; string tooltip = displayText == link.Url ? null : link.Url; From 30bdd2d4c0eaa6922ced5f44c589df93d50c9518 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 1 Dec 2023 01:07:23 +0100 Subject: [PATCH 22/59] Extract `Overlaps()` logic to accept generic index and length --- osu.Game/Online/Chat/MessageFormatter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 9a194dba47..078af667d1 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -364,7 +364,9 @@ namespace osu.Game.Online.Chat Argument = argument; } - public bool Overlaps(Link otherLink) => Index < otherLink.Index + otherLink.Length && otherLink.Index < Index + Length; + public bool Overlaps(Link otherLink) => Overlaps(otherLink.Index, otherLink.Length); + + public bool Overlaps(int index, int length) => Index < index + length && index < Index + Length; public int CompareTo(Link? otherLink) => Index > otherLink?.Index ? 1 : -1; } From d3517998cff60db3d80d38a9fc71460ce1707258 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 1 Dec 2023 01:08:22 +0100 Subject: [PATCH 23/59] Use common `Overlaps()` logic This actually fixes the problem and makes the tests pass --- osu.Game/Online/Chat/MessageFormatter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 078af667d1..c5256c3c74 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -85,8 +85,8 @@ namespace osu.Game.Online.Chat if (escapeChars != null) displayText = escapeChars.Aggregate(displayText, (current, c) => current.Replace($"\\{c}", c.ToString())); - // Check for encapsulated links - if (result.Links.Find(l => (l.Index <= index && l.Index + l.Length >= index + m.Length) || (index <= l.Index && index + m.Length >= l.Index + l.Length)) == null) + // Check for overlapping links + if (!result.Links.Exists(l => l.Overlaps(index, m.Length))) { result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText); From 894c31753b1c1737ef2dd7ecdf7440d9ef739cee Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 1 Dec 2023 15:31:06 +0900 Subject: [PATCH 24/59] Add initial support for aborting multiplayer games --- .../Multiplayer/IMultiplayerRoomServer.cs | 5 +++ .../Online/Multiplayer/MultiplayerClient.cs | 2 ++ .../Multiplayer/OnlineMultiplayerClient.cs | 10 ++++++ .../Multiplayer/Match/ConfirmAbortDialog.cs | 33 +++++++++++++++++++ .../Multiplayer/Match/MatchStartControl.cs | 27 +++++++++++++-- .../Match/MultiplayerReadyButton.cs | 31 ++++++++++++----- .../Multiplayer/TestMultiplayerClient.cs | 6 ++++ 7 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index b7a608581c..15a8b42457 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -82,6 +82,11 @@ namespace osu.Game.Online.Multiplayer /// Task AbortGameplay(); + /// + /// Real. + /// + Task AbortGameplayReal(); + /// /// Adds an item to the playlist. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 79f46c2095..140380d679 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -374,6 +374,8 @@ namespace osu.Game.Online.Multiplayer public abstract Task AbortGameplay(); + public abstract Task AbortGameplayReal(); + public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item); public abstract Task EditPlaylistItem(MultiplayerPlaylistItem item); diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index e400132693..47f4205dfd 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -226,6 +226,16 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.AbortGameplay)); } + public override Task AbortGameplayReal() + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + + return connection.InvokeAsync(nameof(IMultiplayerServer.AbortGameplayReal)); + } + public override Task AddPlaylistItem(MultiplayerPlaylistItem item) { if (!IsConnected.Value) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs new file mode 100644 index 0000000000..8aca96a918 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Sprites; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public partial class ConfirmAbortDialog : PopupDialog + { + public ConfirmAbortDialog(Action onConfirm, Action onCancel) + { + HeaderText = "Are you sure you want to go abort the match?"; + + Icon = FontAwesome.Solid.ExclamationTriangle; + + Buttons = new PopupDialogButton[] + { + new PopupDialogDangerousButton + { + Text = @"Yes", + Action = onConfirm + }, + new PopupDialogCancelButton + { + Text = @"No I didn't mean to", + Action = onCancel + }, + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index 44e18dd2bb..d44878f7c3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Threading; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match @@ -28,6 +29,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [CanBeNull] private IDisposable clickOperation; + [Resolved(canBeNull: true)] + private IDialogOverlay dialogOverlay { get; set; } + private Sample sampleReady; private Sample sampleReadyAll; private Sample sampleUnready; @@ -109,8 +113,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Debug.Assert(clickOperation == null); clickOperation = ongoingOperationTracker.BeginOperation(); - if (isReady() && Client.IsHost && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown)) - startMatch(); + if (Client.IsHost) + { + if (Room.State == MultiplayerRoomState.Open) + { + if (isReady() && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown)) + startMatch(); + else + toggleReady(); + } + else + { + if (dialogOverlay == null) + abortMatch(); + else + dialogOverlay.Push(new ConfirmAbortDialog(abortMatch, endOperation)); + } + } else toggleReady(); @@ -128,6 +147,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match // gameplay was not started due to an exception; unblock button. endOperation(); }); + + void abortMatch() => Client.AbortGameplayReal().FireAndForget(endOperation, _ => endOperation()); } private void startCountdown(TimeSpan duration) @@ -198,6 +219,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (localUser?.State == MultiplayerUserState.Spectating) readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown); + readyButton.Enabled.Value = true; + if (newCountReady == countReady) return; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 1be573bdb8..8a9d027469 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -158,7 +158,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Text = room.Host?.Equals(localUser) == true ? $"Start match {countText}" : $"Waiting for host... {countText}"; + break; + case MultiplayerUserState.Idle: + if (room.State == MultiplayerRoomState.Open || room.Host?.Equals(localUser) != true) + { + Text = "Ready"; + break; + } + + Text = "Abort!"; break; } } @@ -204,17 +213,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match setYellow(); break; + + case MultiplayerUserState.Idle: + if (room.State == MultiplayerRoomState.Open || room.Host?.Equals(localUser) != true) + { + setGreen(); + break; + } + + setRed(); + break; } - void setYellow() - { - BackgroundColour = colours.YellowDark; - } + void setYellow() => BackgroundColour = colours.YellowDark; - void setGreen() - { - BackgroundColour = colours.Green; - } + void setGreen() => BackgroundColour = colours.Green; + + void setRed() => BackgroundColour = colours.Red; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 577104db45..a73c3a72a2 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -396,6 +396,12 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } + public override Task AbortGameplayReal() + { + // Todo: + return Task.CompletedTask; + } + public async Task AddUserPlaylistItem(int userId, MultiplayerPlaylistItem item) { Debug.Assert(ServerRoom != null); From a94180c8c6cde22814da67de2ff0e78be9285609 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 1 Dec 2023 18:26:59 +0900 Subject: [PATCH 25/59] Rename LoadAborted -> GameplayAborted, AbortGameplayReal -> AbortMatch --- .../Online/Multiplayer/GameplayAbortReason.cs | 11 +++++++++++ .../Online/Multiplayer/IMultiplayerClient.cs | 11 ++++++----- .../Online/Multiplayer/IMultiplayerRoomServer.cs | 10 +++++----- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 10 +++++----- .../Multiplayer/OnlineMultiplayerClient.cs | 6 +++--- .../Multiplayer/Match/MatchStartControl.cs | 2 +- .../OnlinePlay/Multiplayer/Multiplayer.cs | 16 +++++++++++++--- .../Visual/Multiplayer/TestMultiplayerClient.cs | 2 +- 8 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 osu.Game/Online/Multiplayer/GameplayAbortReason.cs diff --git a/osu.Game/Online/Multiplayer/GameplayAbortReason.cs b/osu.Game/Online/Multiplayer/GameplayAbortReason.cs new file mode 100644 index 0000000000..15151ea68b --- /dev/null +++ b/osu.Game/Online/Multiplayer/GameplayAbortReason.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Online.Multiplayer +{ + public enum GameplayAbortReason + { + LoadTookTooLong, + HostAbortedTheMatch + } +} diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index a5fa49a94b..0452d8b79c 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -107,17 +107,18 @@ namespace osu.Game.Online.Multiplayer /// Task LoadRequested(); - /// - /// Signals that loading of gameplay is to be aborted. - /// - Task LoadAborted(); - /// /// Signals that gameplay has started. /// All users in the or states should begin gameplay as soon as possible. /// Task GameplayStarted(); + /// + /// Signals that gameplay has been aborted. + /// + /// The reason why gameplay was aborted. + Task GameplayAborted(GameplayAbortReason reason); + /// /// Signals that the match has ended, all players have finished and results are ready to be displayed. /// diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 15a8b42457..55f00b447f 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -77,16 +77,16 @@ namespace osu.Game.Online.Multiplayer /// If an attempt to start the game occurs when the game's (or users') state disallows it. Task StartMatch(); + /// + /// As the host of a room, aborts an on-going match. + /// + Task AbortMatch(); + /// /// Aborts an ongoing gameplay load. /// Task AbortGameplay(); - /// - /// Real. - /// - Task AbortGameplayReal(); - /// /// Adds an item to the playlist. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 140380d679..bbf0e3697a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -73,9 +73,9 @@ namespace osu.Game.Online.Multiplayer public virtual event Action? LoadRequested; /// - /// Invoked when the multiplayer server requests loading of play to be aborted. + /// Invoked when the multiplayer server requests gameplay to be aborted. /// - public event Action? LoadAborted; + public event Action? GameplayAborted; /// /// Invoked when the multiplayer server requests gameplay to be started. @@ -374,7 +374,7 @@ namespace osu.Game.Online.Multiplayer public abstract Task AbortGameplay(); - public abstract Task AbortGameplayReal(); + public abstract Task AbortMatch(); public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item); @@ -684,14 +684,14 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - Task IMultiplayerClient.LoadAborted() + Task IMultiplayerClient.GameplayAborted(GameplayAbortReason reason) { Scheduler.Add(() => { if (Room == null) return; - LoadAborted?.Invoke(); + GameplayAborted?.Invoke(reason); }, false); return Task.CompletedTask; diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 47f4205dfd..40436d730e 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -58,7 +58,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested); connection.On(nameof(IMultiplayerClient.GameplayStarted), ((IMultiplayerClient)this).GameplayStarted); - connection.On(nameof(IMultiplayerClient.LoadAborted), ((IMultiplayerClient)this).LoadAborted); + connection.On(nameof(IMultiplayerClient.GameplayAborted), ((IMultiplayerClient)this).GameplayAborted); connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); connection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged); connection.On(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged); @@ -226,14 +226,14 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.AbortGameplay)); } - public override Task AbortGameplayReal() + public override Task AbortMatch() { if (!IsConnected.Value) return Task.CompletedTask; Debug.Assert(connection != null); - return connection.InvokeAsync(nameof(IMultiplayerServer.AbortGameplayReal)); + return connection.InvokeAsync(nameof(IMultiplayerServer.AbortMatch)); } public override Task AddPlaylistItem(MultiplayerPlaylistItem item) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index d44878f7c3..8ca5d61ab4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -148,7 +148,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match endOperation(); }); - void abortMatch() => Client.AbortGameplayReal().FireAndForget(endOperation, _ => endOperation()); + void abortMatch() => Client.AbortMatch().FireAndForget(endOperation, _ => endOperation()); } private void startCountdown(TimeSpan duration) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index edf5ce276a..7d27725775 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.LoadComplete(); client.RoomUpdated += onRoomUpdated; - client.LoadAborted += onLoadAborted; + client.GameplayAborted += onGameplayAborted; onRoomUpdated(); } @@ -39,12 +39,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer transitionFromResults(); } - private void onLoadAborted() + private void onGameplayAborted(GameplayAbortReason reason) { // If the server aborts gameplay for this user (due to loading too slow), exit gameplay screens. if (!this.IsCurrentScreen()) { - Logger.Log("Gameplay aborted because loading the beatmap took too long.", LoggingTarget.Runtime, LogLevel.Important); + switch (reason) + { + case GameplayAbortReason.LoadTookTooLong: + Logger.Log("Gameplay aborted because loading the beatmap took too long.", LoggingTarget.Runtime, LogLevel.Important); + break; + + case GameplayAbortReason.HostAbortedTheMatch: + Logger.Log("The host aborted the match.", LoggingTarget.Runtime, LogLevel.Important); + break; + } + this.MakeCurrent(); } } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index a73c3a72a2..3af8f9c5db 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -396,7 +396,7 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } - public override Task AbortGameplayReal() + public override Task AbortMatch() { // Todo: return Task.CompletedTask; From 15c9416244a47e9d5fb713a99ddd9ce77b5403bd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 1 Dec 2023 18:47:40 +0900 Subject: [PATCH 26/59] Rename method --- .../Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index 8ca5d61ab4..a21c85e316 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { RelativeSizeAxes = Axes.Both, Size = Vector2.One, - Action = onReadyClick, + Action = onReadyButtonClick, }, countdownButton = new MultiplayerCountdownButton { @@ -105,7 +105,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match endOperation(); } - private void onReadyClick() + private void onReadyButtonClick() { if (Room == null) return; From 1b0fc8ca9d8740d9c61aa20dcbace2f72357cd96 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 1 Dec 2023 19:02:27 +0900 Subject: [PATCH 27/59] Refactor --- .../Multiplayer/Match/ConfirmAbortDialog.cs | 2 +- .../Match/MultiplayerReadyButton.cs | 24 ++++++------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs index 8aca96a918..06f2b2c8f6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs @@ -11,7 +11,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public ConfirmAbortDialog(Action onConfirm, Action onCancel) { - HeaderText = "Are you sure you want to go abort the match?"; + HeaderText = "Are you sure you want to abort the match?"; Icon = FontAwesome.Solid.ExclamationTriangle; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 8a9d027469..368e5210de 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -155,19 +155,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match case MultiplayerUserState.Spectating: case MultiplayerUserState.Ready: - Text = room.Host?.Equals(localUser) == true + Text = multiplayerClient.IsHost ? $"Start match {countText}" : $"Waiting for host... {countText}"; break; - case MultiplayerUserState.Idle: - if (room.State == MultiplayerRoomState.Open || room.Host?.Equals(localUser) != true) - { - Text = "Ready"; - break; - } - - Text = "Abort!"; + // Show the abort button for the host as long as gameplay is in progress. + case MultiplayerUserState when multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open: + Text = "Abort the match"; break; } } @@ -207,20 +202,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match case MultiplayerUserState.Spectating: case MultiplayerUserState.Ready: - if (room?.Host?.Equals(localUser) == true && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown)) + if (multiplayerClient.IsHost && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown)) setGreen(); else setYellow(); break; - case MultiplayerUserState.Idle: - if (room.State == MultiplayerRoomState.Open || room.Host?.Equals(localUser) != true) - { - setGreen(); - break; - } - + // Show the abort button for the host as long as gameplay is in progress. + case MultiplayerUserState when multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open: setRed(); break; } From e0eea07a3fa201cb227856213f7831524d635cef Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 1 Dec 2023 13:00:34 +0100 Subject: [PATCH 28/59] Request `READ_EXTERNAL_STORAGE` on older android versions --- osu.Android/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Android/AndroidManifest.xml b/osu.Android/AndroidManifest.xml index af102a1e4e..2fb4d1f850 100644 --- a/osu.Android/AndroidManifest.xml +++ b/osu.Android/AndroidManifest.xml @@ -5,4 +5,5 @@ + \ No newline at end of file From cdaff30aa6e69582d18d7034728c2c615cbb44fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 1 Dec 2023 13:24:51 +0100 Subject: [PATCH 29/59] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 3b90b1675c..6609db3027 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 7e5c5be4ea..a6b3527466 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From d97ae8df6a916c75c1fca2c9ab80919039c9b3d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 1 Dec 2023 13:31:09 +0100 Subject: [PATCH 30/59] Remove commented out italic code It's a holdover from the Exo days (anyone remember those?) and hasn't been relevant for years, so why keep it. --- osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 8d4ff82303..af7cc003a5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -106,11 +106,8 @@ namespace osu.Game.Tests.Visual.Online }); AddAssert("msg has the right action", () => newLine.Message.Links.Select(l => l.Action), () => Is.EqualTo(expectedActions)); - //AddAssert("msg is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); AddAssert($"msg shows {expectedActions.Length} link(s)", isShowingLinks); - //bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font.Italics); - bool isShowingLinks() { bool hasBackground = !string.IsNullOrEmpty(newLine.Message.Sender.Colour); From f3530a79b18817e3ca1fd005933add63ac75f82e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 1 Dec 2023 21:34:20 +0900 Subject: [PATCH 31/59] Add test --- .../Multiplayer/TestSceneMatchStartControl.cs | 25 +++++++++++++++++++ .../Multiplayer/TestMultiplayerClient.cs | 6 ++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 6d309078e6..f8719ba80c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -378,6 +378,31 @@ namespace osu.Game.Tests.Visual.Multiplayer }, users); } + [Test] + public void TestAbortMatch() + { + multiplayerClient.Setup(m => m.StartMatch()) + .Callback(() => + { + multiplayerClient.Raise(m => m.LoadRequested -= null); + multiplayerClient.Object.Room!.State = MultiplayerRoomState.WaitingForLoad; + + // The local user state doesn't really matter, so let's do the same as the base implementation for these tests. + changeUserState(localUser.UserID, MultiplayerUserState.Idle); + }); + + multiplayerClient.Setup(m => m.AbortMatch()) + .Callback(() => + { + multiplayerClient.Object.Room!.State = MultiplayerRoomState.Open; + raiseRoomUpdated(); + }); + + ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + } + private void verifyGameplayStartFlow() { checkLocalUserState(MultiplayerUserState.Ready); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 3af8f9c5db..4c3deac1d7 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -396,10 +396,10 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } - public override Task AbortMatch() + public override async Task AbortMatch() { - // Todo: - return Task.CompletedTask; + ChangeUserState(api.LocalUser.Value.Id, MultiplayerUserState.Idle); + await ((IMultiplayerClient)this).GameplayAborted(GameplayAbortReason.HostAbortedTheMatch).ConfigureAwait(false); } public async Task AddUserPlaylistItem(int userId, MultiplayerPlaylistItem item) From abb4c943a7f36a5e8fb4ada240cfb66ed433a9e0 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 1 Dec 2023 18:35:57 +0100 Subject: [PATCH 32/59] Rename to more readable names --- osu.Game/Online/Chat/MessageFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index c5256c3c74..f055633d64 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -366,7 +366,7 @@ namespace osu.Game.Online.Chat public bool Overlaps(Link otherLink) => Overlaps(otherLink.Index, otherLink.Length); - public bool Overlaps(int index, int length) => Index < index + length && index < Index + Length; + public bool Overlaps(int otherIndex, int otherLength) => Index < otherIndex + otherLength && otherIndex < Index + Length; public int CompareTo(Link? otherLink) => Index > otherLink?.Index ? 1 : -1; } From 230278f2c94230803f11310b592dcbc15043274c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 3 Dec 2023 01:45:15 +0900 Subject: [PATCH 33/59] Once again remove Mania passive HP drain --- .../ManiaHealthProcessorTest.cs | 31 +++++++++++++++++++ .../Scoring/ManiaHealthProcessor.cs | 8 +++++ 2 files changed, 39 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs new file mode 100644 index 0000000000..315849f7de --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Scoring; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class ManiaHealthProcessorTest + { + [Test] + public void TestNoDrain() + { + var processor = new ManiaHealthProcessor(0); + processor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4)) + { + HitObjects = + { + new Note { StartTime = 0 }, + new Note { StartTime = 1000 }, + } + }); + + // No matter what, mania doesn't have passive HP drain. + Assert.That(processor.DrainRate, Is.Zero); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs index 183550eb7b..34b1787a71 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs @@ -15,6 +15,14 @@ namespace osu.Game.Rulesets.Mania.Scoring { } + protected override double ComputeDrainRate() + { + // Base call is run only to compute HP recovery. + base.ComputeDrainRate(); + + return 0; + } + protected override IEnumerable EnumerateTopLevelHitObjects() => Beatmap.HitObjects; protected override IEnumerable EnumerateNestedHitObjects(HitObject hitObject) => hitObject.NestedHitObjects; From d0acb7f4f9e601f10b7650a217329cfb0b041158 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 4 Dec 2023 06:08:31 +0900 Subject: [PATCH 34/59] Improve commenting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs index 34b1787a71..a33eac83c2 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs @@ -17,7 +17,8 @@ namespace osu.Game.Rulesets.Mania.Scoring protected override double ComputeDrainRate() { - // Base call is run only to compute HP recovery. + // Base call is run only to compute HP recovery (namely, `HpMultiplierNormal`). + // This closely mirrors (broken) behaviour of stable and as such is preserved unchanged. base.ComputeDrainRate(); return 0; From c2644a5d5e208214f383714b606c5e442bc04a28 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 4 Dec 2023 10:18:37 +0900 Subject: [PATCH 35/59] Correctly implement button enabled state --- .../Visual/Multiplayer/TestSceneMatchStartControl.cs | 1 + .../OnlinePlay/Multiplayer/Match/MatchStartControl.cs | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index f8719ba80c..c64dea3f59 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -401,6 +401,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); ClickButtonWhenEnabled(); ClickButtonWhenEnabled(); + AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once)); } private void verifyGameplayStartFlow() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index a21c85e316..e61735fe61 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -130,7 +130,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match dialogOverlay.Push(new ConfirmAbortDialog(abortMatch, endOperation)); } } - else + else if (Room.State != MultiplayerRoomState.Closed) toggleReady(); bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating; @@ -210,7 +210,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } readyButton.Enabled.Value = countdownButton.Enabled.Value = - Room.State == MultiplayerRoomState.Open + Room.State != MultiplayerRoomState.Closed && CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId && !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired && !operationInProgress.Value; @@ -219,7 +219,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (localUser?.State == MultiplayerUserState.Spectating) readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown); - readyButton.Enabled.Value = true; + // When the local user is not the host, the button should only be enabled when no match is in progress. + if (!Client.IsHost) + readyButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open; if (newCountReady == countReady) return; From 9ccd33a1ecc61e684ca92b30781fb55b669a9016 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 4 Dec 2023 10:20:53 +0900 Subject: [PATCH 36/59] Add comments to test --- .../Visual/Multiplayer/TestSceneMatchStartControl.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index c64dea3f59..750968fc75 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -398,8 +398,13 @@ namespace osu.Game.Tests.Visual.Multiplayer raiseRoomUpdated(); }); + // Ready ClickButtonWhenEnabled(); + + // Start match ClickButtonWhenEnabled(); + + // Abort ClickButtonWhenEnabled(); AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once)); } From 8587652869ea92665de36af0f1bc4a9079ab3c8a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 4 Dec 2023 11:00:11 +0900 Subject: [PATCH 37/59] Fix countdown button being enabled --- .../Multiplayer/TestSceneMatchStartControl.cs | 32 +++++++++++-------- .../Multiplayer/Match/MatchStartControl.cs | 3 ++ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 750968fc75..2d61c26a6b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -381,28 +381,32 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestAbortMatch() { - multiplayerClient.Setup(m => m.StartMatch()) - .Callback(() => - { - multiplayerClient.Raise(m => m.LoadRequested -= null); - multiplayerClient.Object.Room!.State = MultiplayerRoomState.WaitingForLoad; + AddStep("setup client", () => + { + multiplayerClient.Setup(m => m.StartMatch()) + .Callback(() => + { + multiplayerClient.Raise(m => m.LoadRequested -= null); + multiplayerClient.Object.Room!.State = MultiplayerRoomState.WaitingForLoad; - // The local user state doesn't really matter, so let's do the same as the base implementation for these tests. - changeUserState(localUser.UserID, MultiplayerUserState.Idle); - }); + // The local user state doesn't really matter, so let's do the same as the base implementation for these tests. + changeUserState(localUser.UserID, MultiplayerUserState.Idle); + }); - multiplayerClient.Setup(m => m.AbortMatch()) - .Callback(() => - { - multiplayerClient.Object.Room!.State = MultiplayerRoomState.Open; - raiseRoomUpdated(); - }); + multiplayerClient.Setup(m => m.AbortMatch()) + .Callback(() => + { + multiplayerClient.Object.Room!.State = MultiplayerRoomState.Open; + raiseRoomUpdated(); + }); + }); // Ready ClickButtonWhenEnabled(); // Start match ClickButtonWhenEnabled(); + AddUntilStep("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); // Abort ClickButtonWhenEnabled(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index e61735fe61..99934acaae 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -223,6 +223,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (!Client.IsHost) readyButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open; + // At all times, the countdown button should only be enabled when no match is in progress. + countdownButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open; + if (newCountReady == countReady) return; From c2a4a6d8cb0cbc8a00b98489b6496f1e843c9afc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Dec 2023 12:10:31 +0900 Subject: [PATCH 38/59] Add inline comment matching framework --- osu.Android/AndroidManifest.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Android/AndroidManifest.xml b/osu.Android/AndroidManifest.xml index 2fb4d1f850..492d48542e 100644 --- a/osu.Android/AndroidManifest.xml +++ b/osu.Android/AndroidManifest.xml @@ -5,5 +5,12 @@ + - \ No newline at end of file + From 7deff70b4ae1710c0d66495d52aafbb6a8b3cd37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Dec 2023 13:34:36 +0100 Subject: [PATCH 39/59] Extract beatmap import steps from gameplay scene switch helper --- .../TestSceneSkinEditorNavigation.cs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index c17a9ddf5f..28855830e1 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -38,6 +38,9 @@ namespace osu.Game.Tests.Visual.Navigation advanceToSongSelect(); openSkinEditor(); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + switchToGameplayScene(); BarHitErrorMeter hitErrorMeter = null; @@ -98,6 +101,10 @@ namespace osu.Game.Tests.Visual.Navigation { advanceToSongSelect(); openSkinEditor(); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + switchToGameplayScene(); AddUntilStep("wait for components", () => skinEditor.ChildrenOfType().Any()); @@ -162,6 +169,9 @@ namespace osu.Game.Tests.Visual.Navigation openSkinEditor(); AddStep("select DT", () => Game.SelectedMods.Value = new Mod[] { new OsuModDoubleTime() }); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + switchToGameplayScene(); AddAssert("DT still selected", () => ((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Single() is OsuModDoubleTime); @@ -174,6 +184,9 @@ namespace osu.Game.Tests.Visual.Navigation openSkinEditor(); AddStep("select relax and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModRelax(), new OsuModSpunOut() }); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + switchToGameplayScene(); AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any()); @@ -186,6 +199,9 @@ namespace osu.Game.Tests.Visual.Navigation openSkinEditor(); AddStep("select autoplay", () => Game.SelectedMods.Value = new Mod[] { new OsuModAutoplay() }); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + switchToGameplayScene(); AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any()); @@ -198,6 +214,9 @@ namespace osu.Game.Tests.Visual.Navigation openSkinEditor(); AddStep("select cinema", () => Game.SelectedMods.Value = new Mod[] { new OsuModCinema() }); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + switchToGameplayScene(); AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any()); @@ -266,9 +285,6 @@ namespace osu.Game.Tests.Visual.Navigation private void switchToGameplayScene() { - AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); - AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); - AddStep("Click gameplay scene button", () => { InputManager.MoveMouseTo(skinEditor.ChildrenOfType().First(b => b.Text.ToString() == "Gameplay")); From d3e94cd5bfaa328032c12c44cb32d08b9b8f8728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Dec 2023 13:50:52 +0100 Subject: [PATCH 40/59] Add test coverage for crashing on empty beatmap --- .../Navigation/TestSceneSkinEditorNavigation.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 28855830e1..62b53f9bac 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -12,9 +12,11 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Threading; +using osu.Game.Online.API; using osu.Game.Overlays.Settings; using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Play; @@ -259,6 +261,19 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("editor sidebars not empty", () => skinEditor.ChildrenOfType().SelectMany(sidebar => sidebar.Children).Count(), () => Is.GreaterThan(0)); } + [Test] + public void TestOpenSkinEditorGameplaySceneOnBeatmapWithNoObjects() + { + AddStep("set dummy beatmap", () => Game.Beatmap.SetDefault()); + advanceToSongSelect(); + + AddStep("create empty beatmap", () => Game.BeatmapManager.CreateNew(new OsuRuleset().RulesetInfo, new GuestUser())); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + openSkinEditor(); + switchToGameplayScene(); + } + private void advanceToSongSelect() { PushAndConfirm(() => songSelect = new TestPlaySongSelect()); From 7c041df0f1875a8274597b626cb465f05143304a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Dec 2023 13:52:41 +0100 Subject: [PATCH 41/59] Add test coverage for crashing on dummy beatmap --- .../Visual/Navigation/TestSceneSkinEditorNavigation.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 62b53f9bac..4424b8cef6 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -274,6 +274,15 @@ namespace osu.Game.Tests.Visual.Navigation switchToGameplayScene(); } + [Test] + public void TestOpenSkinEditorGameplaySceneWhenDummyBeatmapActive() + { + AddStep("set dummy beatmap", () => Game.Beatmap.SetDefault()); + + openSkinEditor(); + switchToGameplayScene(); + } + private void advanceToSongSelect() { PushAndConfirm(() => songSelect = new TestPlaySongSelect()); From 43312619e36c6d57a0c904adfd7a4d12890cf867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Dec 2023 13:58:07 +0100 Subject: [PATCH 42/59] Add test coverage for crashing on wrong ruleset --- .../Navigation/TestSceneSkinEditorNavigation.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 4424b8cef6..74e6ba1566 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -13,6 +13,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Online.API; +using osu.Game.Beatmaps; using osu.Game.Overlays.Settings; using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets.Mods; @@ -283,6 +284,22 @@ namespace osu.Game.Tests.Visual.Navigation switchToGameplayScene(); } + [Test] + public void TestOpenSkinEditorGameplaySceneWhenDifferentRulesetActive() + { + BeatmapSetInfo beatmapSet = null!; + + AddStep("import beatmap", () => beatmapSet = BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely()); + AddStep("select mania difficulty", () => + { + var beatmap = beatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 3); + Game.Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(beatmap); + }); + + openSkinEditor(); + switchToGameplayScene(); + } + private void advanceToSongSelect() { PushAndConfirm(() => songSelect = new TestPlaySongSelect()); From 055fb5bd8f482d4fc23658f259a9f9108cf4bc29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Dec 2023 14:14:07 +0100 Subject: [PATCH 43/59] Do not set initial activity in skin editor endless player Seems like an overreach to say that the user is "watching a replay" there. --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 18d1c4c62b..6e64f5b786 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -26,6 +26,7 @@ using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; using osu.Game.Screens.Select; +using osu.Game.Users; using osu.Game.Utils; using osuTK; @@ -285,6 +286,8 @@ namespace osu.Game.Overlays.SkinEditor private partial class EndlessPlayer : ReplayPlayer { + protected override UserActivity? InitialActivity => null; + public EndlessPlayer(Func, Score> createScore) : base(createScore, new PlayerConfiguration { From 9d39b70e38d6beef46e0ad3a7fad03acf0673dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Dec 2023 14:14:26 +0100 Subject: [PATCH 44/59] Fix endless player not handling load failure --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 6e64f5b786..46658bb993 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -297,10 +297,21 @@ namespace osu.Game.Overlays.SkinEditor { } + protected override void LoadComplete() + { + base.LoadComplete(); + + if (!LoadedBeatmapSuccessfully) + Scheduler.AddDelayed(this.Exit, 3000); + } + protected override void Update() { base.Update(); + if (!LoadedBeatmapSuccessfully) + return; + if (GameplayState.HasPassed) GameplayClockContainer.Seek(0); } From 063694f5444a3d2b22409e45c121a67470800446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Dec 2023 14:20:03 +0100 Subject: [PATCH 45/59] Do not attempt to load gameplay scene if current beatmap is dummy --- .../Visual/Navigation/TestSceneSkinEditorNavigation.cs | 1 - osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 74e6ba1566..fa85c8c9f8 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -281,7 +281,6 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("set dummy beatmap", () => Game.Beatmap.SetDefault()); openSkinEditor(); - switchToGameplayScene(); } [Test] diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 46658bb993..ae118836c8 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -140,6 +140,12 @@ namespace osu.Game.Overlays.SkinEditor { performer?.PerformFromScreen(screen => { + if (beatmap.Value is DummyWorkingBeatmap) + { + // presume we don't have anything good to play and just bail. + return; + } + // If we're playing the intro, switch away to another beatmap. if (beatmap.Value.BeatmapSetInfo.Protected) { From 8754fa40f4251bd5c94752eb3fadccaf2e95a490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Dec 2023 14:23:38 +0100 Subject: [PATCH 46/59] Source autoplay mod from beatmap about to be presented rather than ambient global --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index ae118836c8..d5c0cd89cf 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -157,7 +157,7 @@ namespace osu.Game.Overlays.SkinEditor if (screen is Player) return; - var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod(); + var replayGeneratingMod = beatmap.Value.BeatmapInfo.Ruleset.CreateInstance().GetAutoplayMod(); IReadOnlyList usableMods = mods.Value; From 5512298d60e84950f15ab9d3cc2453e2a0a7a74d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Dec 2023 15:01:23 +0100 Subject: [PATCH 47/59] Trim unused resolved bindable --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index d5c0cd89cf..0821e603d1 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -56,9 +56,6 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private MusicController music { get; set; } = null!; - [Resolved] - private IBindable ruleset { get; set; } = null!; - [Resolved] private Bindable> mods { get; set; } = null!; From c5a08a07118204227dc26a4c1177908ab77f531f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Dec 2023 23:06:08 +0900 Subject: [PATCH 48/59] Remove unused using statement --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 0821e603d1..be7ddd115b 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -17,7 +17,6 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; -using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens; From 0e474928580f369645381dae6a9a1388a3cd0391 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 4 Dec 2023 20:17:22 +0100 Subject: [PATCH 49/59] Uncomment net6.0 code and remove old code --- osu.Game/Overlays/BeatmapListingOverlay.cs | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index f8784504b8..a645683c5f 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -183,9 +183,7 @@ namespace osu.Game.Overlays // new results may contain beatmaps from a previous page, // this is dodgy but matches web behaviour for now. // see: https://github.com/ppy/osu-web/issues/9270 - // todo: replace custom equality compraer with ExceptBy in net6.0 - // newCards = newCards.ExceptBy(foundContent.Select(c => c.BeatmapSet.OnlineID), c => c.BeatmapSet.OnlineID); - newCards = newCards.Except(foundContent, BeatmapCardEqualityComparer.Default); + newCards = newCards.ExceptBy(foundContent.Select(c => c.BeatmapSet.OnlineID), c => c.BeatmapSet.OnlineID); panelLoadTask = LoadComponentsAsync(newCards, loaded => { @@ -378,21 +376,5 @@ namespace osu.Game.Overlays if (shouldShowMore) filterControl.FetchNextPage(); } - - private class BeatmapCardEqualityComparer : IEqualityComparer - { - public static BeatmapCardEqualityComparer Default { get; } = new BeatmapCardEqualityComparer(); - - public bool Equals(BeatmapCard x, BeatmapCard y) - { - if (ReferenceEquals(x, y)) return true; - if (ReferenceEquals(x, null)) return false; - if (ReferenceEquals(y, null)) return false; - - return x.BeatmapSet.Equals(y.BeatmapSet); - } - - public int GetHashCode(BeatmapCard obj) => obj.BeatmapSet.GetHashCode(); - } } } From 8756dd25c6d7cff903b0ec9e2fce1a2af48e197c Mon Sep 17 00:00:00 2001 From: Guido <75315940+vegguid@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:51:56 +0100 Subject: [PATCH 50/59] Changed file chooser in resource selection to show file name when file is selected --- osu.Game/Localisation/EditorSetupStrings.cs | 10 ---------- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 9 ++------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/osu.Game/Localisation/EditorSetupStrings.cs b/osu.Game/Localisation/EditorSetupStrings.cs index 401411365b..eff6f9e6b8 100644 --- a/osu.Game/Localisation/EditorSetupStrings.cs +++ b/osu.Game/Localisation/EditorSetupStrings.cs @@ -179,21 +179,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ClickToSelectTrack => new TranslatableString(getKey(@"click_to_select_track"), @"Click to select a track"); - /// - /// "Click to replace the track" - /// - public static LocalisableString ClickToReplaceTrack => new TranslatableString(getKey(@"click_to_replace_track"), @"Click to replace the track"); - /// /// "Click to select a background image" /// public static LocalisableString ClickToSelectBackground => new TranslatableString(getKey(@"click_to_select_background"), @"Click to select a background image"); - /// - /// "Click to replace the background image" - /// - public static LocalisableString ClickToReplaceBackground => new TranslatableString(getKey(@"click_to_replace_background"), @"Click to replace the background image"); - /// /// "Ruleset ({0})" /// diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 8c84ad90ba..f6d20319cb 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -146,13 +146,8 @@ namespace osu.Game.Screens.Edit.Setup private void updatePlaceholderText() { - audioTrackChooser.Text = audioTrackChooser.Current.Value == null - ? EditorSetupStrings.ClickToSelectTrack - : EditorSetupStrings.ClickToReplaceTrack; - - backgroundChooser.Text = backgroundChooser.Current.Value == null - ? EditorSetupStrings.ClickToSelectBackground - : EditorSetupStrings.ClickToReplaceBackground; + audioTrackChooser.Text = audioTrackChooser.Current.Value?.Name ?? EditorSetupStrings.ClickToSelectTrack; + backgroundChooser.Text = backgroundChooser.Current.Value?.Name ?? EditorSetupStrings.ClickToSelectBackground; } } } From 7fda38d0b02496f4e255e496a73def950deca6a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Dec 2023 14:12:25 +0900 Subject: [PATCH 51/59] Use ordinal comparison when searching at song select Bypasses various overheads. In theory should be fine? (until it's not on some language) --- osu.Game/Screens/Select/FilterCriteria.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 812a16c484..811f623ee5 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -176,13 +176,15 @@ namespace osu.Game.Screens.Select { default: case MatchMode.Substring: - return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase); + // Note that we are using ordinal here to avoid performance issues caused by globalisation concerns. + // See https://github.com/ppy/osu/issues/11571 / https://github.com/dotnet/docs/issues/18423. + return value.Contains(SearchTerm, StringComparison.OrdinalIgnoreCase); case MatchMode.IsolatedPhrase: return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); case MatchMode.FullPhrase: - return CultureInfo.InvariantCulture.CompareInfo.Compare(value, searchTerm, CompareOptions.IgnoreCase) == 0; + return CultureInfo.InvariantCulture.CompareInfo.Compare(value, searchTerm, CompareOptions.OrdinalIgnoreCase) == 0; } } From 27e778ae09364981ce5e2768110efde51b4a3f7b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Dec 2023 14:15:05 +0900 Subject: [PATCH 52/59] Avoid sorting items when already in the correct sort order --- .../Screens/Select/Carousel/CarouselGroup.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs index 9302578038..c353ee98ae 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs @@ -86,16 +86,20 @@ namespace osu.Game.Screens.Select.Carousel items.ForEach(c => c.Filter(criteria)); - criteriaComparer = Comparer.Create((x, y) => + // Sorting is expensive, so only perform if it's actually changed. + if (lastCriteria?.Sort != criteria.Sort) { - int comparison = x.CompareTo(criteria, y); - if (comparison != 0) - return comparison; + criteriaComparer = Comparer.Create((x, y) => + { + int comparison = x.CompareTo(criteria, y); + if (comparison != 0) + return comparison; - return x.ItemID.CompareTo(y.ItemID); - }); + return x.ItemID.CompareTo(y.ItemID); + }); - items.Sort(criteriaComparer); + items.Sort(criteriaComparer); + } lastCriteria = criteria; } From 42010574b5d27d8ca6925a8b21ba7a61e4391926 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Dec 2023 14:53:47 +0900 Subject: [PATCH 53/59] Avoid list construction when doing filtering --- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 22 +++++++++---------- .../Beatmaps/BeatmapMetadataInfoExtensions.cs | 19 +++++++++++++++- .../Select/Carousel/CarouselBeatmap.cs | 21 +----------------- 3 files changed, 29 insertions(+), 33 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index 3aab9a24e1..a3bc03acc8 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Localisation; +using osu.Game.Screens.Select; namespace osu.Game.Beatmaps { @@ -29,20 +29,18 @@ namespace osu.Game.Beatmaps return new RomanisableString($"{metadata.GetPreferred(true)}".Trim(), $"{metadata.GetPreferred(false)}".Trim()); } - public static List GetSearchableTerms(this IBeatmapInfo beatmapInfo) + public static bool Match(this IBeatmapInfo beatmapInfo, params FilterCriteria.OptionalTextFilter[] filters) { - var termsList = new List(BeatmapMetadataInfoExtensions.MAX_SEARCHABLE_TERM_COUNT + 1); - - addIfNotNull(beatmapInfo.DifficultyName); - - BeatmapMetadataInfoExtensions.CollectSearchableTerms(beatmapInfo.Metadata, termsList); - return termsList; - - void addIfNotNull(string? s) + foreach (var filter in filters) { - if (!string.IsNullOrEmpty(s)) - termsList.Add(s); + if (filter.Matches(beatmapInfo.DifficultyName)) + return true; + + if (BeatmapMetadataInfoExtensions.Match(beatmapInfo.Metadata, filters)) + return true; } + + return false; } private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]"; diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs index be96a66614..ee3afdaef5 100644 --- a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs @@ -3,11 +3,14 @@ using System.Collections.Generic; using osu.Framework.Localisation; +using osu.Game.Screens.Select; namespace osu.Game.Beatmaps { public static class BeatmapMetadataInfoExtensions { + internal const int MAX_SEARCHABLE_TERM_COUNT = 7; + /// /// An array of all searchable terms provided in contained metadata. /// @@ -18,7 +21,21 @@ namespace osu.Game.Beatmaps return termsList.ToArray(); } - internal const int MAX_SEARCHABLE_TERM_COUNT = 7; + public static bool Match(IBeatmapMetadataInfo metadataInfo, FilterCriteria.OptionalTextFilter[] filters) + { + foreach (var filter in filters) + { + if (filter.Matches(metadataInfo.Author.Username)) return true; + if (filter.Matches(metadataInfo.Artist)) return true; + if (filter.Matches(metadataInfo.ArtistUnicode)) return true; + if (filter.Matches(metadataInfo.Title)) return true; + if (filter.Matches(metadataInfo.TitleUnicode)) return true; + if (filter.Matches(metadataInfo.Source)) return true; + if (filter.Matches(metadataInfo.Tags)) return true; + } + + return false; + } internal static void CollectSearchableTerms(IBeatmapMetadataInfo metadataInfo, IList termsList) { diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 1d40862df7..8b891a035c 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -66,26 +66,7 @@ namespace osu.Game.Screens.Select.Carousel if (criteria.SearchTerms.Length > 0) { - var searchableTerms = BeatmapInfo.GetSearchableTerms(); - - foreach (FilterCriteria.OptionalTextFilter criteriaTerm in criteria.SearchTerms) - { - bool any = false; - - // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator - foreach (string searchTerm in searchableTerms) - { - if (!criteriaTerm.Matches(searchTerm)) continue; - - any = true; - break; - } - - if (any) continue; - - match = false; - break; - } + match = BeatmapInfo.Match(criteria.SearchTerms); // if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs. // this should be done after text matching so we can prioritise matching numbers in metadata. From 45e499778f8696087defd6c116970f918428539f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Dec 2023 15:28:56 +0900 Subject: [PATCH 54/59] Search terms before performing other criteria checks Very minor, but putting the more common case towards the start of the method allows early return. --- .../Select/Carousel/CarouselBeatmap.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 8b891a035c..45594bd0e8 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -41,6 +41,21 @@ namespace osu.Game.Screens.Select.Carousel return match; } + if (criteria.SearchTerms.Length > 0) + { + match = BeatmapInfo.Match(criteria.SearchTerms); + + // if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs. + // this should be done after text matching so we can prioritise matching numbers in metadata. + if (!match && criteria.SearchNumber.HasValue) + { + match = (BeatmapInfo.OnlineID == criteria.SearchNumber.Value) || + (BeatmapInfo.BeatmapSet?.OnlineID == criteria.SearchNumber.Value); + } + } + + if (!match) return false; + match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarRating); match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(BeatmapInfo.Difficulty.ApproachRate); match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.Difficulty.DrainRate); @@ -64,21 +79,6 @@ namespace osu.Game.Screens.Select.Carousel if (!match) return false; - if (criteria.SearchTerms.Length > 0) - { - match = BeatmapInfo.Match(criteria.SearchTerms); - - // if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs. - // this should be done after text matching so we can prioritise matching numbers in metadata. - if (!match && criteria.SearchNumber.HasValue) - { - match = (BeatmapInfo.OnlineID == criteria.SearchNumber.Value) || - (BeatmapInfo.BeatmapSet?.OnlineID == criteria.SearchNumber.Value); - } - } - - if (!match) return false; - match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true; if (match && criteria.RulesetCriteria != null) match &= criteria.RulesetCriteria.Matches(BeatmapInfo); From f317e06da14040d2fb5fbbc7375bbf12c729c1e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Dec 2023 16:54:44 +0900 Subject: [PATCH 55/59] Use `DangerousActionDialog` --- .../Overlays/Dialog/DangerousActionDialog.cs | 8 ++++++- .../Multiplayer/Match/ConfirmAbortDialog.cs | 22 ++----------------- .../Multiplayer/Match/MatchStartControl.cs | 12 ++++++++++ 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs index c86570386f..42a3ff827c 100644 --- a/osu.Game/Overlays/Dialog/DangerousActionDialog.cs +++ b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs @@ -23,6 +23,11 @@ namespace osu.Game.Overlays.Dialog /// protected Action? DangerousAction { get; set; } + /// + /// The action to perform if cancelled. + /// + protected Action? CancelAction { get; set; } + protected DangerousActionDialog() { HeaderText = DeleteConfirmationDialogStrings.HeaderText; @@ -38,7 +43,8 @@ namespace osu.Game.Overlays.Dialog }, new PopupDialogCancelButton { - Text = DeleteConfirmationDialogStrings.Cancel + Text = DeleteConfirmationDialogStrings.Cancel, + Action = () => CancelAction?.Invoke() } }; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs index 06f2b2c8f6..0793981f41 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs @@ -1,33 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using osu.Framework.Graphics.Sprites; using osu.Game.Overlays.Dialog; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public partial class ConfirmAbortDialog : PopupDialog + public partial class ConfirmAbortDialog : DangerousActionDialog { - public ConfirmAbortDialog(Action onConfirm, Action onCancel) + public ConfirmAbortDialog() { HeaderText = "Are you sure you want to abort the match?"; - - Icon = FontAwesome.Solid.ExclamationTriangle; - - Buttons = new PopupDialogButton[] - { - new PopupDialogDangerousButton - { - Text = @"Yes", - Action = onConfirm - }, - new PopupDialogCancelButton - { - Text = @"No I didn't mean to", - Action = onCancel - }, - }; } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index 99934acaae..ba3508b24f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -17,6 +17,7 @@ using osu.Framework.Threading; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match @@ -247,5 +248,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match countReady = newCountReady; }); } + + public partial class ConfirmAbortDialog : DangerousActionDialog + { + public ConfirmAbortDialog(Action abortMatch, Action cancel) + { + HeaderText = "Are you sure you want to abort the match?"; + + DangerousAction = abortMatch; + CancelAction = cancel; + } + } } } From 02178d8e611dfc3c8a8335f41c68d6fab5dfbe0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Dec 2023 16:58:16 +0900 Subject: [PATCH 56/59] Remove usage of `case-when` --- .../Match/MultiplayerReadyButton.cs | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 368e5210de..7ce3dde7c2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -149,10 +149,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { switch (localUser?.State) { - default: - Text = "Ready"; - break; - case MultiplayerUserState.Spectating: case MultiplayerUserState.Ready: Text = multiplayerClient.IsHost @@ -160,9 +156,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match : $"Waiting for host... {countText}"; break; - // Show the abort button for the host as long as gameplay is in progress. - case MultiplayerUserState when multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open: - Text = "Abort the match"; + default: + // Show the abort button for the host as long as gameplay is in progress. + if (multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open) + Text = "Abort the match"; + else + Text = "Ready"; break; } } @@ -197,7 +196,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match switch (localUser?.State) { default: - setGreen(); + // Show the abort button for the host as long as gameplay is in progress. + if (multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open) + setRed(); + else + setGreen(); break; case MultiplayerUserState.Spectating: @@ -208,11 +211,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match setYellow(); break; - - // Show the abort button for the host as long as gameplay is in progress. - case MultiplayerUserState when multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open: - setRed(); - break; } void setYellow() => BackgroundColour = colours.YellowDark; From 8704dc3505a934f42f2259ec734bb072aa940282 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Dec 2023 18:20:27 +0900 Subject: [PATCH 57/59] Fix change in filter behaviour --- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 12 ++++++++---- .../Beatmaps/BeatmapMetadataInfoExtensions.cs | 19 ++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index a3bc03acc8..b00d0ba316 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -34,13 +34,17 @@ namespace osu.Game.Beatmaps foreach (var filter in filters) { if (filter.Matches(beatmapInfo.DifficultyName)) - return true; + continue; - if (BeatmapMetadataInfoExtensions.Match(beatmapInfo.Metadata, filters)) - return true; + if (BeatmapMetadataInfoExtensions.Match(beatmapInfo.Metadata, filter)) + continue; + + // failed to match a single filter at all - fail the whole match. + return false; } - return false; + // got through all filters without failing any - pass the whole match. + return true; } private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]"; diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs index ee3afdaef5..198469dba6 100644 --- a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs @@ -21,18 +21,15 @@ namespace osu.Game.Beatmaps return termsList.ToArray(); } - public static bool Match(IBeatmapMetadataInfo metadataInfo, FilterCriteria.OptionalTextFilter[] filters) + public static bool Match(IBeatmapMetadataInfo metadataInfo, FilterCriteria.OptionalTextFilter filter) { - foreach (var filter in filters) - { - if (filter.Matches(metadataInfo.Author.Username)) return true; - if (filter.Matches(metadataInfo.Artist)) return true; - if (filter.Matches(metadataInfo.ArtistUnicode)) return true; - if (filter.Matches(metadataInfo.Title)) return true; - if (filter.Matches(metadataInfo.TitleUnicode)) return true; - if (filter.Matches(metadataInfo.Source)) return true; - if (filter.Matches(metadataInfo.Tags)) return true; - } + if (filter.Matches(metadataInfo.Author.Username)) return true; + if (filter.Matches(metadataInfo.Artist)) return true; + if (filter.Matches(metadataInfo.ArtistUnicode)) return true; + if (filter.Matches(metadataInfo.Title)) return true; + if (filter.Matches(metadataInfo.TitleUnicode)) return true; + if (filter.Matches(metadataInfo.Source)) return true; + if (filter.Matches(metadataInfo.Tags)) return true; return false; } From 4644c4e7a2c8676df418def1b6630e63253ebd81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Dec 2023 12:43:32 +0100 Subject: [PATCH 58/59] Remove unused class --- .../Multiplayer/Match/ConfirmAbortDialog.cs | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs deleted file mode 100644 index 0793981f41..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ConfirmAbortDialog.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Overlays.Dialog; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match -{ - public partial class ConfirmAbortDialog : DangerousActionDialog - { - public ConfirmAbortDialog() - { - HeaderText = "Are you sure you want to abort the match?"; - } - } -} From 07f9f5c6d842d3c7c564f96576682b6fb54c50b4 Mon Sep 17 00:00:00 2001 From: POeticPotatoes Date: Wed, 6 Dec 2023 06:33:25 +0800 Subject: [PATCH 59/59] Remove hover checks for mod-copying menu item --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 136c9cc8e7..a76f4ae955 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -421,7 +421,7 @@ namespace osu.Game.Online.Leaderboards { List items = new List(); - if (Score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null) + if (Score.Mods.Length > 0 && songSelect != null) items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods)); if (Score.Files.Count > 0)