From f53ce5aedfd17d18eaa9a882e14707636a806a92 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sun, 23 Jan 2022 11:11:12 +0800 Subject: [PATCH 001/285] Fix max combo calculation in osu diffcalc --- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index c5b1baaad1..c80b19e1d3 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -63,8 +63,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty double drainRate = beatmap.Difficulty.DrainRate; int maxCombo = beatmap.HitObjects.Count; - // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) - maxCombo += beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1); + // Add the ticks + tail of the slider + // 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) + // an additional 1 is subtracted if only nested objects are judged because the hit result of the entire slider would not contribute to combo + maxCombo += beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1 - (s.OnlyJudgeNestedObjects ? 1 : 0)); int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); int sliderCount = beatmap.HitObjects.Count(h => h is Slider); From 44311c1f4e3e18548f9b574de968468d52f8c282 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sun, 23 Jan 2022 11:25:22 +0800 Subject: [PATCH 002/285] Add tests for diffcalc max combo --- .../CatchDifficultyCalculatorTest.cs | 12 +++++------ .../ManiaDifficultyCalculatorTest.cs | 12 +++++------ .../OsuDifficultyCalculatorTest.cs | 21 ++++++++++++------- .../TaikoDifficultyCalculatorTest.cs | 16 +++++++------- .../Beatmaps/DifficultyCalculatorTest.cs | 7 +++++-- 5 files changed, 38 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs index 7e8d567fbe..48d46636df 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; - [TestCase(4.0505463516206195d, "diffcalc-test")] - public void Test(double expected, string name) - => base.Test(expected, name); + [TestCase(4.0505463516206195d, 127, "diffcalc-test")] + public void Test(double expectedStarRating, int expectedMaxCombo, string name) + => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(5.1696411260785498d, "diffcalc-test")] - public void TestClockRateAdjusted(double expected, string name) - => Test(expected, name, new CatchModDoubleTime()); + [TestCase(5.1696411260785498d, 127, "diffcalc-test")] + public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) + => Test(expectedStarRating, expectedMaxCombo, name, new CatchModDoubleTime()); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset().RulesetInfo, beatmap); diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs index 6ec49d7634..715614a201 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Mania.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; - [TestCase(2.3449735700206298d, "diffcalc-test")] - public void Test(double expected, string name) - => base.Test(expected, name); + [TestCase(2.3449735700206298d, 151, "diffcalc-test")] + public void Test(double expectedStarRating, int expectedMaxCombo, string name) + => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(2.7879104989252959d, "diffcalc-test")] - public void TestClockRateAdjusted(double expected, string name) - => Test(expected, name, new ManiaModDoubleTime()); + [TestCase(2.7879104989252959d, 151, "diffcalc-test")] + public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) + => Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime()); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, beatmap); diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index b7984e6995..df577ea8d3 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,15 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; - [TestCase(6.6972307565739273d, "diffcalc-test")] - [TestCase(1.4484754139145539d, "zero-length-sliders")] - public void Test(double expected, string name) - => base.Test(expected, name); + [TestCase(6.6972307565739273d, 206, "diffcalc-test")] + [TestCase(1.4484754139145539d, 45, "zero-length-sliders")] + public void Test(double expectedStarRating, int expectedMaxCombo, string name) + => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(8.9382559208689809d, "diffcalc-test")] - [TestCase(1.7548875851757628d, "zero-length-sliders")] - public void TestClockRateAdjusted(double expected, string name) - => Test(expected, name, new OsuModDoubleTime()); + [TestCase(8.9382559208689809d, 206, "diffcalc-test")] + [TestCase(1.7548875851757628d, 45, "zero-length-sliders")] + public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) + => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); + + [TestCase(6.6972307218715166d, 239, "diffcalc-test")] + [TestCase(1.4484754139145537d, 54, "zero-length-sliders")] + public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) + => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap); diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 2b1cbc580e..226da7df09 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,15 +14,15 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(2.2420075288523802d, "diffcalc-test")] - [TestCase(2.2420075288523802d, "diffcalc-test-strong")] - public void Test(double expected, string name) - => base.Test(expected, name); + [TestCase(2.2420075288523802d, 200, "diffcalc-test")] + [TestCase(2.2420075288523802d, 200, "diffcalc-test-strong")] + public void Test(double expectedStarRating, int expectedMaxCombo, string name) + => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(3.134084469440479d, "diffcalc-test")] - [TestCase(3.134084469440479d, "diffcalc-test-strong")] - public void TestClockRateAdjusted(double expected, string name) - => Test(expected, name, new TaikoModDoubleTime()); + [TestCase(3.134084469440479d, 200, "diffcalc-test")] + [TestCase(3.134084469440479d, 200, "diffcalc-test-strong")] + public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) + => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset().RulesetInfo, beatmap); diff --git a/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs b/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs index 9f8811c7f9..ed00c7959b 100644 --- a/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs +++ b/osu.Game/Tests/Beatmaps/DifficultyCalculatorTest.cs @@ -22,10 +22,13 @@ namespace osu.Game.Tests.Beatmaps protected abstract string ResourceAssembly { get; } - protected void Test(double expected, string name, params Mod[] mods) + protected void Test(double expectedStarRating, int expectedMaxCombo, string name, params Mod[] mods) { + var attributes = CreateDifficultyCalculator(getBeatmap(name)).Calculate(mods); + // Platform-dependent math functions (Pow, Cbrt, Exp, etc) may result in minute differences. - Assert.That(CreateDifficultyCalculator(getBeatmap(name)).Calculate(mods).StarRating, Is.EqualTo(expected).Within(0.00001)); + Assert.That(attributes.StarRating, Is.EqualTo(expectedStarRating).Within(0.00001)); + Assert.That(attributes.MaxCombo, Is.EqualTo(expectedMaxCombo)); } private IWorkingBeatmap getBeatmap(string name) From 74a55ead7711108c4d6b856e11433b476459c35a Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sun, 23 Jan 2022 13:00:54 +0800 Subject: [PATCH 003/285] Simplify combo counting logic --- .../Difficulty/OsuDifficultyCalculator.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index c80b19e1d3..d04d0872d8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; @@ -62,11 +63,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double drainRate = beatmap.Difficulty.DrainRate; - int maxCombo = beatmap.HitObjects.Count; - // Add the ticks + tail of the slider - // 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) - // an additional 1 is subtracted if only nested objects are judged because the hit result of the entire slider would not contribute to combo - maxCombo += beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1 - (s.OnlyJudgeNestedObjects ? 1 : 0)); + int maxCombo = 0; + + void countCombo(HitObject ho) + { + if (ho.CreateJudgement().MaxResult.AffectsCombo()) + maxCombo++; + } + + foreach (HitObject ho in beatmap.HitObjects) + { + countCombo(ho); + foreach (HitObject nested in ho.NestedHitObjects) + countCombo(nested); + } int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); int sliderCount = beatmap.HitObjects.Count(h => h is Slider); From 215da7e933ded0423618e6fb6f58e28c65ea2339 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 16 Feb 2022 12:05:55 +0900 Subject: [PATCH 004/285] Reimplement as extension method on IBeatmap Implementation has changed slightly to support arbitrary levels of nested hitobjects. --- .../Difficulty/OsuDifficultyCalculator.cs | 17 +------------ osu.Game/Beatmaps/IBeatmap.cs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index d04d0872d8..df6fd19d36 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -9,7 +9,6 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; @@ -62,21 +61,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double drainRate = beatmap.Difficulty.DrainRate; - - int maxCombo = 0; - - void countCombo(HitObject ho) - { - if (ho.CreateJudgement().MaxResult.AffectsCombo()) - maxCombo++; - } - - foreach (HitObject ho in beatmap.HitObjects) - { - countCombo(ho); - foreach (HitObject nested in ho.NestedHitObjects) - countCombo(nested); - } + int maxCombo = beatmap.GetMaxCombo(); int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); int sliderCount = beatmap.HitObjects.Count(h => h is Slider); diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 3f598cd1e5..dec1ef4294 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Beatmaps { @@ -70,4 +71,27 @@ namespace osu.Game.Beatmaps /// new IReadOnlyList HitObjects { get; } } + + public static class BeatmapExtensions + { + /// + /// Finds the maximum achievable combo by hitting all s in a beatmap. + /// + public static int GetMaxCombo(this IBeatmap beatmap) + { + int combo = 0; + foreach (var h in beatmap.HitObjects) + addCombo(h, ref combo); + return combo; + + static void addCombo(HitObject hitObject, ref int combo) + { + if (hitObject.CreateJudgement().MaxResult.AffectsCombo()) + combo++; + + foreach (var nested in hitObject.NestedHitObjects) + addCombo(nested, ref combo); + } + } + } } From 3945cd24ebb84579fe1492c083d820155581f652 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Feb 2022 21:14:49 +0900 Subject: [PATCH 005/285] wip --- .../Rulesets/Difficulty/DifficultyCalculator.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 6b61dd3efb..7d6c235fc1 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Utils; namespace osu.Game.Rulesets.Difficulty { @@ -122,12 +123,17 @@ namespace osu.Game.Rulesets.Difficulty /// A collection of structures describing the difficulty of the beatmap for each mod combination. public IEnumerable CalculateAll(CancellationToken cancellationToken = default) { + var rulesetInstance = ruleset.CreateInstance(); + foreach (var combination in CreateDifficultyAdjustmentModCombinations()) { - if (combination is MultiMod multi) - yield return Calculate(multi.Mods, cancellationToken); - else - yield return Calculate(combination.Yield(), cancellationToken); + Mod classicMod = rulesetInstance.CreateAllMods().SingleOrDefault(m => m is ModClassic); + + var finalCombination = ModUtils.FlattenMod(combination); + if (classicMod != null) + finalCombination = finalCombination.Append(classicMod); + + yield return Calculate(finalCombination.ToArray(), cancellationToken); } } From bedd07d2e4dca93e160949614f9f1a75f03a2fe2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 22 Feb 2022 18:12:55 +0900 Subject: [PATCH 006/285] Add remark about usage of CalculateAll() --- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 7d6c235fc1..0935f26de6 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -120,6 +120,9 @@ namespace osu.Game.Rulesets.Difficulty /// /// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap. /// + /// + /// This should only be used to compute difficulties for legacy mod combinations. + /// /// A collection of structures describing the difficulty of the beatmap for each mod combination. public IEnumerable CalculateAll(CancellationToken cancellationToken = default) { From 37328f8d245fc9d50e3a14bfe57a3d9c73d0f1d1 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 9 Mar 2022 20:36:31 +0800 Subject: [PATCH 007/285] Extract hit object positioning logic to a separate class It is intentional to not rename the identifiers at this point to produce a cleaner diff. --- .../Mods/OsuHitObjectPositionModifier.cs | 346 ++++++++++++++++++ osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 325 +--------------- 2 files changed, 354 insertions(+), 317 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs b/osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs new file mode 100644 index 0000000000..3242b99755 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs @@ -0,0 +1,346 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics.Primitives; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Osu.Utils; +using osuTK; + +#nullable enable + +namespace osu.Game.Rulesets.Osu.Mods +{ + /// + /// Places hit objects according to information in while keeping objects inside the playfield. + /// + public class OsuHitObjectPositionModifier + { + /// + /// Number of previous hitobjects to be shifted together when an object is being moved. + /// + private const int preceding_hitobjects_to_shift = 10; + + private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2; + + private readonly List hitObjects; + + private readonly List randomObjects = new List(); + + /// + /// Contains information specifying how each hit object should be placed. + /// The default values correspond to how objects are originally placed in the beatmap. + /// + public IReadOnlyList RandomObjects => randomObjects; + + public OsuHitObjectPositionModifier(List hitObjects) + { + this.hitObjects = hitObjects; + populateHitObjectPositions(); + } + + private void populateHitObjectPositions() + { + Vector2 previousPosition = playfield_centre; + float previousAngle = 0; + + foreach (OsuHitObject hitObject in hitObjects) + { + Vector2 relativePosition = hitObject.Position - previousPosition; + float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); + float relativeAngle = absoluteAngle - previousAngle; + + randomObjects.Add(new RandomObjectInfo(hitObject) + { + RelativeAngle = relativeAngle, + DistanceFromPrevious = relativePosition.Length + }); + + previousPosition = hitObject.EndPosition; + previousAngle = absoluteAngle; + } + } + + /// + /// Reposition the hit objects according to the information in . + /// + public void ApplyRandomisation() + { + RandomObjectInfo? previous = null; + + for (int i = 0; i < hitObjects.Count; i++) + { + var hitObject = hitObjects[i]; + + var current = randomObjects[i]; + + if (hitObject is Spinner) + { + previous = null; + continue; + } + + computeRandomisedPosition(current, previous, i > 1 ? randomObjects[i - 2] : null); + + // Move hit objects back into the playfield if they are outside of it + Vector2 shift = Vector2.Zero; + + switch (hitObject) + { + case HitCircle circle: + shift = clampHitCircleToPlayfield(circle, current); + break; + + case Slider slider: + shift = clampSliderToPlayfield(slider, current); + break; + } + + if (shift != Vector2.Zero) + { + var toBeShifted = new List(); + + for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--) + { + // only shift hit circles + if (!(hitObjects[j] is HitCircle)) break; + + toBeShifted.Add(hitObjects[j]); + } + + if (toBeShifted.Count > 0) + applyDecreasingShift(toBeShifted, shift); + } + + previous = current; + } + } + + /// + /// Compute the randomised position of a hit object while attempting to keep it inside the playfield. + /// + /// The representing the hit object to have the randomised position computed for. + /// The representing the hit object immediately preceding the current one. + /// The representing the hit object immediately preceding the one. + private void computeRandomisedPosition(RandomObjectInfo current, RandomObjectInfo? previous, RandomObjectInfo? beforePrevious) + { + float previousAbsoluteAngle = 0f; + + if (previous != null) + { + Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre; + Vector2 relativePosition = previous.HitObject.Position - earliestPosition; + previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); + } + + float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle; + + var posRelativeToPrev = new Vector2( + current.DistanceFromPrevious * (float)Math.Cos(absoluteAngle), + current.DistanceFromPrevious * (float)Math.Sin(absoluteAngle) + ); + + Vector2 lastEndPosition = previous?.EndPositionRandomised ?? playfield_centre; + + posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastEndPosition, posRelativeToPrev); + + current.PositionRandomised = lastEndPosition + posRelativeToPrev; + } + + /// + /// Move the randomised position of a hit circle so that it fits inside the playfield. + /// + /// The deviation from the original randomised position in order to fit within the playfield. + private Vector2 clampHitCircleToPlayfield(HitCircle circle, RandomObjectInfo objectInfo) + { + var previousPosition = objectInfo.PositionRandomised; + objectInfo.EndPositionRandomised = objectInfo.PositionRandomised = clampToPlayfieldWithPadding( + objectInfo.PositionRandomised, + (float)circle.Radius + ); + + circle.Position = objectInfo.PositionRandomised; + + return objectInfo.PositionRandomised - previousPosition; + } + + /// + /// Moves the and all necessary nested s into the if they aren't already. + /// + /// The deviation from the original randomised position in order to fit within the playfield. + private Vector2 clampSliderToPlayfield(Slider slider, RandomObjectInfo objectInfo) + { + var possibleMovementBounds = calculatePossibleMovementBounds(slider); + + var previousPosition = objectInfo.PositionRandomised; + + // Clamp slider position to the placement area + // If the slider is larger than the playfield, force it to stay at the original position + float newX = possibleMovementBounds.Width < 0 + ? objectInfo.PositionOriginal.X + : Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right); + + float newY = possibleMovementBounds.Height < 0 + ? objectInfo.PositionOriginal.Y + : Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom); + + slider.Position = objectInfo.PositionRandomised = new Vector2(newX, newY); + objectInfo.EndPositionRandomised = slider.EndPosition; + + shiftNestedObjects(slider, objectInfo.PositionRandomised - objectInfo.PositionOriginal); + + return objectInfo.PositionRandomised - previousPosition; + } + + /// + /// Decreasingly shift a list of s by a specified amount. + /// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount. + /// + /// The list of hit objects to be shifted. + /// The amount to be shifted. + private void applyDecreasingShift(IList hitObjects, Vector2 shift) + { + for (int i = 0; i < hitObjects.Count; i++) + { + var hitObject = hitObjects[i]; + // The first object is shifted by a vector slightly smaller than shift + // The last object is shifted by a vector slightly larger than zero + Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1)); + + hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius); + } + } + + /// + /// Calculates a which contains all of the possible movements of the slider (in relative X/Y coordinates) + /// such that the entire slider is inside the playfield. + /// + /// + /// If the slider is larger than the playfield, the returned may have negative width/height. + /// + private RectangleF calculatePossibleMovementBounds(Slider slider) + { + var pathPositions = new List(); + slider.Path.GetPathToProgress(pathPositions, 0, 1); + + float minX = float.PositiveInfinity; + float maxX = float.NegativeInfinity; + + float minY = float.PositiveInfinity; + float maxY = float.NegativeInfinity; + + // Compute the bounding box of the slider. + foreach (var pos in pathPositions) + { + minX = MathF.Min(minX, pos.X); + maxX = MathF.Max(maxX, pos.X); + + minY = MathF.Min(minY, pos.Y); + maxY = MathF.Max(maxY, pos.Y); + } + + // Take the circle radius into account. + float radius = (float)slider.Radius; + + minX -= radius; + minY -= radius; + + maxX += radius; + maxY += radius; + + // Given the bounding box of the slider (via min/max X/Y), + // the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right), + // and the amount that it can move to the right is WIDTH - maxX. + // Same calculation applies for the Y axis. + float left = -minX; + float right = OsuPlayfield.BASE_SIZE.X - maxX; + float top = -minY; + float bottom = OsuPlayfield.BASE_SIZE.Y - maxY; + + return new RectangleF(left, top, right - left, bottom - top); + } + + /// + /// Shifts all nested s and s by the specified shift. + /// + /// whose nested s and s should be shifted + /// The the 's nested s and s should be shifted by + private void shiftNestedObjects(Slider slider, Vector2 shift) + { + foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat)) + { + if (!(hitObject is OsuHitObject osuHitObject)) + continue; + + osuHitObject.Position += shift; + } + } + + /// + /// Clamp a position to playfield, keeping a specified distance from the edges. + /// + /// The position to be clamped. + /// The minimum distance allowed from playfield edges. + /// The clamped position. + private Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding) + { + return new Vector2( + Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding), + Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding) + ); + } + + public interface IRandomObjectInfo + { + /// + /// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle. + /// + /// + /// of the first hit object in a beatmap represents the absolute angle from playfield center to the object. + /// + /// + /// If is 0, the player's cursor doesn't need to change its direction of movement when passing + /// the previous object to reach this one. + /// + float RelativeAngle { get; set; } + + /// + /// The jump distance from the previous hit object to this one. + /// + /// + /// of the first hit object in a beatmap is relative to the playfield center. + /// + float DistanceFromPrevious { get; set; } + + /// + /// The hit object associated with this . + /// + OsuHitObject HitObject { get; } + } + + private class RandomObjectInfo : IRandomObjectInfo + { + public float RelativeAngle { get; set; } + + public float DistanceFromPrevious { get; set; } + + public Vector2 PositionOriginal { get; } + public Vector2 PositionRandomised { get; set; } + + public Vector2 EndPositionOriginal { get; } + public Vector2 EndPositionRandomised { get; set; } + + public OsuHitObject HitObject { get; } + + public RandomObjectInfo(OsuHitObject hitObject) + { + PositionRandomised = PositionOriginal = hitObject.Position; + EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition; + HitObject = hitObject; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 7479c3120a..2c38be6c16 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -4,19 +4,13 @@ #nullable enable using System; -using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using osu.Framework.Graphics.Primitives; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.Osu.Utils; -using osuTK; namespace osu.Game.Rulesets.Osu.Mods { @@ -28,12 +22,6 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "It never gets boring!"; private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast; - private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2; - - /// - /// Number of previous hitobjects to be shifted together when another object is being moved. - /// - private const int preceding_hitobjects_to_shift = 10; private Random? rng; @@ -42,330 +30,33 @@ namespace osu.Game.Rulesets.Osu.Mods if (!(beatmap is OsuBeatmap osuBeatmap)) return; - var hitObjects = osuBeatmap.HitObjects; - Seed.Value ??= RNG.Next(); rng = new Random((int)Seed.Value); - var randomObjects = randomiseObjects(hitObjects); + var positionModifier = new OsuHitObjectPositionModifier(osuBeatmap.HitObjects); - applyRandomisation(hitObjects, randomObjects); - } - - /// - /// Randomise the position of each hit object and return a list of s describing how each hit object should be placed. - /// - /// A list of s to have their positions randomised. - /// A list of s describing how each hit object should be placed. - private List randomiseObjects(IEnumerable hitObjects) - { - Debug.Assert(rng != null, $"{nameof(ApplyToBeatmap)} was not called before randomising objects"); - - var randomObjects = new List(); - RandomObjectInfo? previous = null; float rateOfChangeMultiplier = 0; - foreach (OsuHitObject hitObject in hitObjects) + foreach (var positionInfo in positionModifier.RandomObjects) { - var current = new RandomObjectInfo(hitObject); - randomObjects.Add(current); - // rateOfChangeMultiplier only changes every 5 iterations in a combo // to prevent shaky-line-shaped streams - if (hitObject.IndexInCurrentCombo % 5 == 0) + if (positionInfo.HitObject.IndexInCurrentCombo % 5 == 0) rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; - if (previous == null) + if (positionInfo == positionModifier.RandomObjects.First()) { - current.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2); - current.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); + positionInfo.DistanceFromPrevious = (float)rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2; + positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); } else { - current.DistanceFromPrevious = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal); - - // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) - // is proportional to the distance between the last and the current hit object - // to allow jumps and prevent too sharp turns during streams. - - // Allow maximum jump angle when jump distance is more than half of playfield diagonal length - current.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, current.DistanceFromPrevious / (playfield_diagonal * 0.5f)); + positionInfo.RelativeAngle = (float)(rateOfChangeMultiplier * 2 * Math.PI * Math.Min(1f, positionInfo.DistanceFromPrevious / (playfield_diagonal * 0.5f))); } - - previous = current; } - return randomObjects; - } - - /// - /// Reposition the hit objects according to the information in . - /// - /// The hit objects to be repositioned. - /// A list of describing how each hit object should be placed. - private void applyRandomisation(IReadOnlyList hitObjects, IReadOnlyList randomObjects) - { - RandomObjectInfo? previous = null; - - for (int i = 0; i < hitObjects.Count; i++) - { - var hitObject = hitObjects[i]; - - var current = randomObjects[i]; - - if (hitObject is Spinner) - { - previous = null; - continue; - } - - computeRandomisedPosition(current, previous, i > 1 ? randomObjects[i - 2] : null); - - // Move hit objects back into the playfield if they are outside of it - Vector2 shift = Vector2.Zero; - - switch (hitObject) - { - case HitCircle circle: - shift = clampHitCircleToPlayfield(circle, current); - break; - - case Slider slider: - shift = clampSliderToPlayfield(slider, current); - break; - } - - if (shift != Vector2.Zero) - { - var toBeShifted = new List(); - - for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--) - { - // only shift hit circles - if (!(hitObjects[j] is HitCircle)) break; - - toBeShifted.Add(hitObjects[j]); - } - - if (toBeShifted.Count > 0) - applyDecreasingShift(toBeShifted, shift); - } - - previous = current; - } - } - - /// - /// Compute the randomised position of a hit object while attempting to keep it inside the playfield. - /// - /// The representing the hit object to have the randomised position computed for. - /// The representing the hit object immediately preceding the current one. - /// The representing the hit object immediately preceding the one. - private void computeRandomisedPosition(RandomObjectInfo current, RandomObjectInfo? previous, RandomObjectInfo? beforePrevious) - { - float previousAbsoluteAngle = 0f; - - if (previous != null) - { - Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre; - Vector2 relativePosition = previous.HitObject.Position - earliestPosition; - previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); - } - - float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle; - - var posRelativeToPrev = new Vector2( - current.DistanceFromPrevious * (float)Math.Cos(absoluteAngle), - current.DistanceFromPrevious * (float)Math.Sin(absoluteAngle) - ); - - Vector2 lastEndPosition = previous?.EndPositionRandomised ?? playfield_centre; - - posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastEndPosition, posRelativeToPrev); - - current.PositionRandomised = lastEndPosition + posRelativeToPrev; - } - - /// - /// Move the randomised position of a hit circle so that it fits inside the playfield. - /// - /// The deviation from the original randomised position in order to fit within the playfield. - private Vector2 clampHitCircleToPlayfield(HitCircle circle, RandomObjectInfo objectInfo) - { - var previousPosition = objectInfo.PositionRandomised; - objectInfo.EndPositionRandomised = objectInfo.PositionRandomised = clampToPlayfieldWithPadding( - objectInfo.PositionRandomised, - (float)circle.Radius - ); - - circle.Position = objectInfo.PositionRandomised; - - return objectInfo.PositionRandomised - previousPosition; - } - - /// - /// Moves the and all necessary nested s into the if they aren't already. - /// - /// The deviation from the original randomised position in order to fit within the playfield. - private Vector2 clampSliderToPlayfield(Slider slider, RandomObjectInfo objectInfo) - { - var possibleMovementBounds = calculatePossibleMovementBounds(slider); - - var previousPosition = objectInfo.PositionRandomised; - - // Clamp slider position to the placement area - // If the slider is larger than the playfield, force it to stay at the original position - float newX = possibleMovementBounds.Width < 0 - ? objectInfo.PositionOriginal.X - : Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right); - - float newY = possibleMovementBounds.Height < 0 - ? objectInfo.PositionOriginal.Y - : Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom); - - slider.Position = objectInfo.PositionRandomised = new Vector2(newX, newY); - objectInfo.EndPositionRandomised = slider.EndPosition; - - shiftNestedObjects(slider, objectInfo.PositionRandomised - objectInfo.PositionOriginal); - - return objectInfo.PositionRandomised - previousPosition; - } - - /// - /// Decreasingly shift a list of s by a specified amount. - /// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount. - /// - /// The list of hit objects to be shifted. - /// The amount to be shifted. - private void applyDecreasingShift(IList hitObjects, Vector2 shift) - { - for (int i = 0; i < hitObjects.Count; i++) - { - var hitObject = hitObjects[i]; - // The first object is shifted by a vector slightly smaller than shift - // The last object is shifted by a vector slightly larger than zero - Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1)); - - hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius); - } - } - - /// - /// Calculates a which contains all of the possible movements of the slider (in relative X/Y coordinates) - /// such that the entire slider is inside the playfield. - /// - /// - /// If the slider is larger than the playfield, the returned may have negative width/height. - /// - private RectangleF calculatePossibleMovementBounds(Slider slider) - { - var pathPositions = new List(); - slider.Path.GetPathToProgress(pathPositions, 0, 1); - - float minX = float.PositiveInfinity; - float maxX = float.NegativeInfinity; - - float minY = float.PositiveInfinity; - float maxY = float.NegativeInfinity; - - // Compute the bounding box of the slider. - foreach (var pos in pathPositions) - { - minX = MathF.Min(minX, pos.X); - maxX = MathF.Max(maxX, pos.X); - - minY = MathF.Min(minY, pos.Y); - maxY = MathF.Max(maxY, pos.Y); - } - - // Take the circle radius into account. - float radius = (float)slider.Radius; - - minX -= radius; - minY -= radius; - - maxX += radius; - maxY += radius; - - // Given the bounding box of the slider (via min/max X/Y), - // the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right), - // and the amount that it can move to the right is WIDTH - maxX. - // Same calculation applies for the Y axis. - float left = -minX; - float right = OsuPlayfield.BASE_SIZE.X - maxX; - float top = -minY; - float bottom = OsuPlayfield.BASE_SIZE.Y - maxY; - - return new RectangleF(left, top, right - left, bottom - top); - } - - /// - /// Shifts all nested s and s by the specified shift. - /// - /// whose nested s and s should be shifted - /// The the 's nested s and s should be shifted by - private void shiftNestedObjects(Slider slider, Vector2 shift) - { - foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat)) - { - if (!(hitObject is OsuHitObject osuHitObject)) - continue; - - osuHitObject.Position += shift; - } - } - - /// - /// Clamp a position to playfield, keeping a specified distance from the edges. - /// - /// The position to be clamped. - /// The minimum distance allowed from playfield edges. - /// The clamped position. - private Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding) - { - return new Vector2( - Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding), - Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding) - ); - } - - private class RandomObjectInfo - { - /// - /// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle. - /// - /// - /// of the first hit object in a beatmap represents the absolute angle from playfield center to the object. - /// - /// - /// If is 0, the player's cursor doesn't need to change its direction of movement when passing - /// the previous object to reach this one. - /// - public float RelativeAngle { get; set; } - - /// - /// The jump distance from the previous hit object to this one. - /// - /// - /// of the first hit object in a beatmap is relative to the playfield center. - /// - public float DistanceFromPrevious { get; set; } - - public Vector2 PositionOriginal { get; } - public Vector2 PositionRandomised { get; set; } - - public Vector2 EndPositionOriginal { get; } - public Vector2 EndPositionRandomised { get; set; } - - public OsuHitObject HitObject { get; } - - public RandomObjectInfo(OsuHitObject hitObject) - { - PositionRandomised = PositionOriginal = hitObject.Position; - EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition; - HitObject = hitObject; - } + positionModifier.ApplyRandomisation(); } } } From 6a507ca11bdf4c95cba876be61bacbc4a9e1a9d5 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 9 Mar 2022 20:52:11 +0800 Subject: [PATCH 008/285] Rename identifiers to remove references to random mod --- .../Mods/OsuHitObjectPositionModifier.cs | 86 +++++++++---------- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 6 +- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs b/osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs index 3242b99755..84ad198951 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Mods { /// - /// Places hit objects according to information in while keeping objects inside the playfield. + /// Places hit objects according to information in while keeping objects inside the playfield. /// public class OsuHitObjectPositionModifier { @@ -28,21 +28,21 @@ namespace osu.Game.Rulesets.Osu.Mods private readonly List hitObjects; - private readonly List randomObjects = new List(); + private readonly List objectPositionInfos = new List(); /// /// Contains information specifying how each hit object should be placed. /// The default values correspond to how objects are originally placed in the beatmap. /// - public IReadOnlyList RandomObjects => randomObjects; + public IReadOnlyList ObjectPositionInfos => objectPositionInfos; public OsuHitObjectPositionModifier(List hitObjects) { this.hitObjects = hitObjects; - populateHitObjectPositions(); + populateObjectPositionInfos(); } - private void populateHitObjectPositions() + private void populateObjectPositionInfos() { Vector2 previousPosition = playfield_centre; float previousAngle = 0; @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Mods float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); float relativeAngle = absoluteAngle - previousAngle; - randomObjects.Add(new RandomObjectInfo(hitObject) + objectPositionInfos.Add(new ObjectPositionInfo(hitObject) { RelativeAngle = relativeAngle, DistanceFromPrevious = relativePosition.Length @@ -65,17 +65,17 @@ namespace osu.Game.Rulesets.Osu.Mods } /// - /// Reposition the hit objects according to the information in . + /// Reposition the hit objects according to the information in . /// - public void ApplyRandomisation() + public void ApplyModifications() { - RandomObjectInfo? previous = null; + ObjectPositionInfo? previous = null; for (int i = 0; i < hitObjects.Count; i++) { var hitObject = hitObjects[i]; - var current = randomObjects[i]; + var current = objectPositionInfos[i]; if (hitObject is Spinner) { @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Mods continue; } - computeRandomisedPosition(current, previous, i > 1 ? randomObjects[i - 2] : null); + computeModifiedPosition(current, previous, i > 1 ? objectPositionInfos[i - 2] : null); // Move hit objects back into the playfield if they are outside of it Vector2 shift = Vector2.Zero; @@ -120,12 +120,12 @@ namespace osu.Game.Rulesets.Osu.Mods } /// - /// Compute the randomised position of a hit object while attempting to keep it inside the playfield. + /// Compute the modified position of a hit object while attempting to keep it inside the playfield. /// - /// The representing the hit object to have the randomised position computed for. - /// The representing the hit object immediately preceding the current one. - /// The representing the hit object immediately preceding the one. - private void computeRandomisedPosition(RandomObjectInfo current, RandomObjectInfo? previous, RandomObjectInfo? beforePrevious) + /// The representing the hit object to have the modified position computed for. + /// The representing the hit object immediately preceding the current one. + /// The representing the hit object immediately preceding the one. + private void computeModifiedPosition(ObjectPositionInfo current, ObjectPositionInfo? previous, ObjectPositionInfo? beforePrevious) { float previousAbsoluteAngle = 0f; @@ -143,56 +143,56 @@ namespace osu.Game.Rulesets.Osu.Mods current.DistanceFromPrevious * (float)Math.Sin(absoluteAngle) ); - Vector2 lastEndPosition = previous?.EndPositionRandomised ?? playfield_centre; + Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre; posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastEndPosition, posRelativeToPrev); - current.PositionRandomised = lastEndPosition + posRelativeToPrev; + current.PositionModified = lastEndPosition + posRelativeToPrev; } /// - /// Move the randomised position of a hit circle so that it fits inside the playfield. + /// Move the modified position of a hit circle so that it fits inside the playfield. /// - /// The deviation from the original randomised position in order to fit within the playfield. - private Vector2 clampHitCircleToPlayfield(HitCircle circle, RandomObjectInfo objectInfo) + /// The deviation from the original modified position in order to fit within the playfield. + private Vector2 clampHitCircleToPlayfield(HitCircle circle, ObjectPositionInfo objectPositionInfo) { - var previousPosition = objectInfo.PositionRandomised; - objectInfo.EndPositionRandomised = objectInfo.PositionRandomised = clampToPlayfieldWithPadding( - objectInfo.PositionRandomised, + var previousPosition = objectPositionInfo.PositionModified; + objectPositionInfo.EndPositionModified = objectPositionInfo.PositionModified = clampToPlayfieldWithPadding( + objectPositionInfo.PositionModified, (float)circle.Radius ); - circle.Position = objectInfo.PositionRandomised; + circle.Position = objectPositionInfo.PositionModified; - return objectInfo.PositionRandomised - previousPosition; + return objectPositionInfo.PositionModified - previousPosition; } /// /// Moves the and all necessary nested s into the if they aren't already. /// - /// The deviation from the original randomised position in order to fit within the playfield. - private Vector2 clampSliderToPlayfield(Slider slider, RandomObjectInfo objectInfo) + /// The deviation from the original modified position in order to fit within the playfield. + private Vector2 clampSliderToPlayfield(Slider slider, ObjectPositionInfo objectPositionInfo) { var possibleMovementBounds = calculatePossibleMovementBounds(slider); - var previousPosition = objectInfo.PositionRandomised; + var previousPosition = objectPositionInfo.PositionModified; // Clamp slider position to the placement area // If the slider is larger than the playfield, force it to stay at the original position float newX = possibleMovementBounds.Width < 0 - ? objectInfo.PositionOriginal.X + ? objectPositionInfo.PositionOriginal.X : Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right); float newY = possibleMovementBounds.Height < 0 - ? objectInfo.PositionOriginal.Y + ? objectPositionInfo.PositionOriginal.Y : Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom); - slider.Position = objectInfo.PositionRandomised = new Vector2(newX, newY); - objectInfo.EndPositionRandomised = slider.EndPosition; + slider.Position = objectPositionInfo.PositionModified = new Vector2(newX, newY); + objectPositionInfo.EndPositionModified = slider.EndPosition; - shiftNestedObjects(slider, objectInfo.PositionRandomised - objectInfo.PositionOriginal); + shiftNestedObjects(slider, objectPositionInfo.PositionModified - objectPositionInfo.PositionOriginal); - return objectInfo.PositionRandomised - previousPosition; + return objectPositionInfo.PositionModified - previousPosition; } /// @@ -293,7 +293,7 @@ namespace osu.Game.Rulesets.Osu.Mods ); } - public interface IRandomObjectInfo + public interface IObjectPositionInfo { /// /// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle. @@ -316,29 +316,29 @@ namespace osu.Game.Rulesets.Osu.Mods float DistanceFromPrevious { get; set; } /// - /// The hit object associated with this . + /// The hit object associated with this . /// OsuHitObject HitObject { get; } } - private class RandomObjectInfo : IRandomObjectInfo + private class ObjectPositionInfo : IObjectPositionInfo { public float RelativeAngle { get; set; } public float DistanceFromPrevious { get; set; } public Vector2 PositionOriginal { get; } - public Vector2 PositionRandomised { get; set; } + public Vector2 PositionModified { get; set; } public Vector2 EndPositionOriginal { get; } - public Vector2 EndPositionRandomised { get; set; } + public Vector2 EndPositionModified { get; set; } public OsuHitObject HitObject { get; } - public RandomObjectInfo(OsuHitObject hitObject) + public ObjectPositionInfo(OsuHitObject hitObject) { - PositionRandomised = PositionOriginal = hitObject.Position; - EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition; + PositionModified = PositionOriginal = hitObject.Position; + EndPositionModified = EndPositionOriginal = hitObject.EndPosition; HitObject = hitObject; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 2c38be6c16..59abc73ed9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -38,14 +38,14 @@ namespace osu.Game.Rulesets.Osu.Mods float rateOfChangeMultiplier = 0; - foreach (var positionInfo in positionModifier.RandomObjects) + foreach (var positionInfo in positionModifier.ObjectPositionInfos) { // rateOfChangeMultiplier only changes every 5 iterations in a combo // to prevent shaky-line-shaped streams if (positionInfo.HitObject.IndexInCurrentCombo % 5 == 0) rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; - if (positionInfo == positionModifier.RandomObjects.First()) + if (positionInfo == positionModifier.ObjectPositionInfos.First()) { positionInfo.DistanceFromPrevious = (float)rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2; positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Mods } } - positionModifier.ApplyRandomisation(); + positionModifier.ApplyModifications(); } } } From 8e12a067dfb1695c984d1d4705aff15c5af18b8b Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 9 Mar 2022 21:04:35 +0800 Subject: [PATCH 009/285] Remove an unused property --- osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs b/osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs index 84ad198951..16ec25f389 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs @@ -329,8 +329,6 @@ namespace osu.Game.Rulesets.Osu.Mods public Vector2 PositionOriginal { get; } public Vector2 PositionModified { get; set; } - - public Vector2 EndPositionOriginal { get; } public Vector2 EndPositionModified { get; set; } public OsuHitObject HitObject { get; } @@ -338,7 +336,7 @@ namespace osu.Game.Rulesets.Osu.Mods public ObjectPositionInfo(OsuHitObject hitObject) { PositionModified = PositionOriginal = hitObject.Position; - EndPositionModified = EndPositionOriginal = hitObject.EndPosition; + EndPositionModified = hitObject.EndPosition; HitObject = hitObject; } } From e8dbed738e4b819f5a68bf9b1df6a2d1b2a824a7 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 9 Mar 2022 21:52:15 +0800 Subject: [PATCH 010/285] Move `OsuHitObjectPositionModifier` to `Utils/` --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 1 + .../{Mods => Utils}/OsuHitObjectPositionModifier.cs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game.Rulesets.Osu/{Mods => Utils}/OsuHitObjectPositionModifier.cs (99%) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 59abc73ed9..cdaa8fa3d5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Osu.Utils; namespace osu.Game.Rulesets.Osu.Mods { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectPositionModifier.cs similarity index 99% rename from osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs rename to osu.Game.Rulesets.Osu/Utils/OsuHitObjectPositionModifier.cs index 16ec25f389..428866623f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuHitObjectPositionModifier.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectPositionModifier.cs @@ -7,12 +7,11 @@ using System.Linq; using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.Osu.Utils; using osuTK; #nullable enable -namespace osu.Game.Rulesets.Osu.Mods +namespace osu.Game.Rulesets.Osu.Utils { /// /// Places hit objects according to information in while keeping objects inside the playfield. From ede838c4b3ebaa2fa2471de68ec11bbf7fbd21e5 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 10 Mar 2022 11:23:52 +0800 Subject: [PATCH 011/285] Use `ObjectPositionInfo.HitObject` --- osu.Game.Rulesets.Osu/Utils/OsuHitObjectPositionModifier.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectPositionModifier.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectPositionModifier.cs index 428866623f..32f547dfe7 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectPositionModifier.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectPositionModifier.cs @@ -70,11 +70,10 @@ namespace osu.Game.Rulesets.Osu.Utils { ObjectPositionInfo? previous = null; - for (int i = 0; i < hitObjects.Count; i++) + for (int i = 0; i < objectPositionInfos.Count; i++) { - var hitObject = hitObjects[i]; - var current = objectPositionInfos[i]; + var hitObject = current.HitObject; if (hitObject is Spinner) { From 3a71d817758e8387568a0a8f974f6c8ac22e43e7 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 10 Mar 2022 11:53:03 +0800 Subject: [PATCH 012/285] Convert the position modifier to stateless methods --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 8 +-- .../Utils/OsuHitObjectGenerationUtils.cs | 2 +- ...OsuHitObjectGenerationUtils_Reposition.cs} | 65 +++++++++---------- 3 files changed, 35 insertions(+), 40 deletions(-) rename osu.Game.Rulesets.Osu/Utils/{OsuHitObjectPositionModifier.cs => OsuHitObjectGenerationUtils_Reposition.cs} (84%) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index cdaa8fa3d5..3c2c5d7759 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -35,18 +35,18 @@ namespace osu.Game.Rulesets.Osu.Mods rng = new Random((int)Seed.Value); - var positionModifier = new OsuHitObjectPositionModifier(osuBeatmap.HitObjects); + var positionInfos = OsuHitObjectGenerationUtils.GeneratePositionInfos(osuBeatmap.HitObjects); float rateOfChangeMultiplier = 0; - foreach (var positionInfo in positionModifier.ObjectPositionInfos) + foreach (var positionInfo in positionInfos) { // rateOfChangeMultiplier only changes every 5 iterations in a combo // to prevent shaky-line-shaped streams if (positionInfo.HitObject.IndexInCurrentCombo % 5 == 0) rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; - if (positionInfo == positionModifier.ObjectPositionInfos.First()) + if (positionInfo == positionInfos.First()) { positionInfo.DistanceFromPrevious = (float)rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2; positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Mods } } - positionModifier.ApplyModifications(); + osuBeatmap.HitObjects = OsuHitObjectGenerationUtils.RepositionHitObjects(positionInfos); } } } diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index 97a4b14a62..da73c2addb 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -11,7 +11,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Utils { - public static class OsuHitObjectGenerationUtils + public static partial class OsuHitObjectGenerationUtils { // The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle. // The closer the hit objects draw to the border, the sharper the turn diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectPositionModifier.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs similarity index 84% rename from osu.Game.Rulesets.Osu/Utils/OsuHitObjectPositionModifier.cs rename to osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index 32f547dfe7..2a735c89d9 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectPositionModifier.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -13,10 +13,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Utils { - /// - /// Places hit objects according to information in while keeping objects inside the playfield. - /// - public class OsuHitObjectPositionModifier + public static partial class OsuHitObjectGenerationUtils { /// /// Number of previous hitobjects to be shifted together when an object is being moved. @@ -25,24 +22,15 @@ namespace osu.Game.Rulesets.Osu.Utils private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2; - private readonly List hitObjects; - - private readonly List objectPositionInfos = new List(); - /// - /// Contains information specifying how each hit object should be placed. - /// The default values correspond to how objects are originally placed in the beatmap. + /// Generate a list of s containing information for how the given list of + /// s are positioned. /// - public IReadOnlyList ObjectPositionInfos => objectPositionInfos; - - public OsuHitObjectPositionModifier(List hitObjects) - { - this.hitObjects = hitObjects; - populateObjectPositionInfos(); - } - - private void populateObjectPositionInfos() + /// A list of s to process. + /// A list of s describing how each hit object is positioned relative to the previous one. + public static List GeneratePositionInfos(IEnumerable hitObjects) { + var positionInfos = new List(); Vector2 previousPosition = playfield_centre; float previousAngle = 0; @@ -52,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Utils float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); float relativeAngle = absoluteAngle - previousAngle; - objectPositionInfos.Add(new ObjectPositionInfo(hitObject) + positionInfos.Add(new ObjectPositionInfo(hitObject) { RelativeAngle = relativeAngle, DistanceFromPrevious = relativePosition.Length @@ -61,18 +49,23 @@ namespace osu.Game.Rulesets.Osu.Utils previousPosition = hitObject.EndPosition; previousAngle = absoluteAngle; } + + return positionInfos; } /// - /// Reposition the hit objects according to the information in . + /// Reposition the hit objects according to the information in . /// - public void ApplyModifications() + /// + /// The repositioned hit objects. + public static List RepositionHitObjects(IEnumerable objectPositionInfos) { + List positionInfos = objectPositionInfos.Cast().ToList(); ObjectPositionInfo? previous = null; - for (int i = 0; i < objectPositionInfos.Count; i++) + for (int i = 0; i < positionInfos.Count; i++) { - var current = objectPositionInfos[i]; + var current = positionInfos[i]; var hitObject = current.HitObject; if (hitObject is Spinner) @@ -81,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Utils continue; } - computeModifiedPosition(current, previous, i > 1 ? objectPositionInfos[i - 2] : null); + computeModifiedPosition(current, previous, i > 1 ? positionInfos[i - 2] : null); // Move hit objects back into the playfield if they are outside of it Vector2 shift = Vector2.Zero; @@ -104,9 +97,9 @@ namespace osu.Game.Rulesets.Osu.Utils for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--) { // only shift hit circles - if (!(hitObjects[j] is HitCircle)) break; + if (!(positionInfos[j].HitObject is HitCircle)) break; - toBeShifted.Add(hitObjects[j]); + toBeShifted.Add(positionInfos[j].HitObject); } if (toBeShifted.Count > 0) @@ -115,6 +108,8 @@ namespace osu.Game.Rulesets.Osu.Utils previous = current; } + + return positionInfos.Select(p => p.HitObject).ToList(); } /// @@ -123,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// The representing the hit object to have the modified position computed for. /// The representing the hit object immediately preceding the current one. /// The representing the hit object immediately preceding the one. - private void computeModifiedPosition(ObjectPositionInfo current, ObjectPositionInfo? previous, ObjectPositionInfo? beforePrevious) + private static void computeModifiedPosition(ObjectPositionInfo current, ObjectPositionInfo? previous, ObjectPositionInfo? beforePrevious) { float previousAbsoluteAngle = 0f; @@ -143,7 +138,7 @@ namespace osu.Game.Rulesets.Osu.Utils Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre; - posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastEndPosition, posRelativeToPrev); + posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev); current.PositionModified = lastEndPosition + posRelativeToPrev; } @@ -152,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// Move the modified position of a hit circle so that it fits inside the playfield. /// /// The deviation from the original modified position in order to fit within the playfield. - private Vector2 clampHitCircleToPlayfield(HitCircle circle, ObjectPositionInfo objectPositionInfo) + private static Vector2 clampHitCircleToPlayfield(HitCircle circle, ObjectPositionInfo objectPositionInfo) { var previousPosition = objectPositionInfo.PositionModified; objectPositionInfo.EndPositionModified = objectPositionInfo.PositionModified = clampToPlayfieldWithPadding( @@ -169,7 +164,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// Moves the and all necessary nested s into the if they aren't already. /// /// The deviation from the original modified position in order to fit within the playfield. - private Vector2 clampSliderToPlayfield(Slider slider, ObjectPositionInfo objectPositionInfo) + private static Vector2 clampSliderToPlayfield(Slider slider, ObjectPositionInfo objectPositionInfo) { var possibleMovementBounds = calculatePossibleMovementBounds(slider); @@ -199,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// /// The list of hit objects to be shifted. /// The amount to be shifted. - private void applyDecreasingShift(IList hitObjects, Vector2 shift) + private static void applyDecreasingShift(IList hitObjects, Vector2 shift) { for (int i = 0; i < hitObjects.Count; i++) { @@ -219,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// /// If the slider is larger than the playfield, the returned may have negative width/height. /// - private RectangleF calculatePossibleMovementBounds(Slider slider) + private static RectangleF calculatePossibleMovementBounds(Slider slider) { var pathPositions = new List(); slider.Path.GetPathToProgress(pathPositions, 0, 1); @@ -266,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// /// whose nested s and s should be shifted /// The the 's nested s and s should be shifted by - private void shiftNestedObjects(Slider slider, Vector2 shift) + private static void shiftNestedObjects(Slider slider, Vector2 shift) { foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat)) { @@ -283,7 +278,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// The position to be clamped. /// The minimum distance allowed from playfield edges. /// The clamped position. - private Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding) + private static Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding) { return new Vector2( Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding), From 5e36383258b8a5cb01ab6298d6d2283ef86e6c4f Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 10 Mar 2022 12:02:25 +0800 Subject: [PATCH 013/285] Convert `IObjectPositionInfo` to a class --- .../OsuHitObjectGenerationUtils_Reposition.cs | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index 2a735c89d9..37a12b20b4 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -23,14 +23,14 @@ namespace osu.Game.Rulesets.Osu.Utils private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2; /// - /// Generate a list of s containing information for how the given list of + /// Generate a list of s containing information for how the given list of /// s are positioned. /// /// A list of s to process. - /// A list of s describing how each hit object is positioned relative to the previous one. - public static List GeneratePositionInfos(IEnumerable hitObjects) + /// A list of s describing how each hit object is positioned relative to the previous one. + public static List GeneratePositionInfos(IEnumerable hitObjects) { - var positionInfos = new List(); + var positionInfos = new List(); Vector2 previousPosition = playfield_centre; float previousAngle = 0; @@ -56,12 +56,12 @@ namespace osu.Game.Rulesets.Osu.Utils /// /// Reposition the hit objects according to the information in . /// - /// + /// Position information for each hit object. /// The repositioned hit objects. - public static List RepositionHitObjects(IEnumerable objectPositionInfos) + public static List RepositionHitObjects(IEnumerable objectPositionInfos) { - List positionInfos = objectPositionInfos.Cast().ToList(); - ObjectPositionInfo? previous = null; + List positionInfos = objectPositionInfos.Select(o => new ObjectPositionInfoInternal(o)).ToList(); + ObjectPositionInfoInternal? previous = null; for (int i = 0; i < positionInfos.Count; i++) { @@ -115,10 +115,10 @@ namespace osu.Game.Rulesets.Osu.Utils /// /// Compute the modified position of a hit object while attempting to keep it inside the playfield. /// - /// The representing the hit object to have the modified position computed for. - /// The representing the hit object immediately preceding the current one. - /// The representing the hit object immediately preceding the one. - private static void computeModifiedPosition(ObjectPositionInfo current, ObjectPositionInfo? previous, ObjectPositionInfo? beforePrevious) + /// The representing the hit object to have the modified position computed for. + /// The representing the hit object immediately preceding the current one. + /// The representing the hit object immediately preceding the one. + private static void computeModifiedPosition(ObjectPositionInfoInternal current, ObjectPositionInfoInternal? previous, ObjectPositionInfoInternal? beforePrevious) { float previousAbsoluteAngle = 0f; @@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// Move the modified position of a hit circle so that it fits inside the playfield. /// /// The deviation from the original modified position in order to fit within the playfield. - private static Vector2 clampHitCircleToPlayfield(HitCircle circle, ObjectPositionInfo objectPositionInfo) + private static Vector2 clampHitCircleToPlayfield(HitCircle circle, ObjectPositionInfoInternal objectPositionInfo) { var previousPosition = objectPositionInfo.PositionModified; objectPositionInfo.EndPositionModified = objectPositionInfo.PositionModified = clampToPlayfieldWithPadding( @@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// Moves the and all necessary nested s into the if they aren't already. /// /// The deviation from the original modified position in order to fit within the playfield. - private static Vector2 clampSliderToPlayfield(Slider slider, ObjectPositionInfo objectPositionInfo) + private static Vector2 clampSliderToPlayfield(Slider slider, ObjectPositionInfoInternal objectPositionInfo) { var possibleMovementBounds = calculatePossibleMovementBounds(slider); @@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.Utils ); } - public interface IObjectPositionInfo + public class ObjectPositionInfo { /// /// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle. @@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// If is 0, the player's cursor doesn't need to change its direction of movement when passing /// the previous object to reach this one. /// - float RelativeAngle { get; set; } + public float RelativeAngle { get; set; } /// /// The jump distance from the previous hit object to this one. @@ -306,32 +306,33 @@ namespace osu.Game.Rulesets.Osu.Utils /// /// of the first hit object in a beatmap is relative to the playfield center. /// - float DistanceFromPrevious { get; set; } - - /// - /// The hit object associated with this . - /// - OsuHitObject HitObject { get; } - } - - private class ObjectPositionInfo : IObjectPositionInfo - { - public float RelativeAngle { get; set; } - public float DistanceFromPrevious { get; set; } - public Vector2 PositionOriginal { get; } - public Vector2 PositionModified { get; set; } - public Vector2 EndPositionModified { get; set; } - + /// + /// The hit object associated with this . + /// public OsuHitObject HitObject { get; } public ObjectPositionInfo(OsuHitObject hitObject) { - PositionModified = PositionOriginal = hitObject.Position; - EndPositionModified = hitObject.EndPosition; HitObject = hitObject; } } + + private class ObjectPositionInfoInternal : ObjectPositionInfo + { + public Vector2 PositionOriginal { get; } + public Vector2 PositionModified { get; set; } + public Vector2 EndPositionModified { get; set; } + + public ObjectPositionInfoInternal(ObjectPositionInfo original) + : base(original.HitObject) + { + RelativeAngle = original.RelativeAngle; + DistanceFromPrevious = original.DistanceFromPrevious; + PositionModified = PositionOriginal = HitObject.Position; + EndPositionModified = HitObject.EndPosition; + } + } } } From 6657d93b29e2dbeda325333c674fd53c572b0d66 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 14 Mar 2022 20:18:30 +0800 Subject: [PATCH 014/285] Separate the two nested classes --- .../OsuHitObjectGenerationUtils_Reposition.cs | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index 37a12b20b4..94f4f154bd 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -60,12 +60,12 @@ namespace osu.Game.Rulesets.Osu.Utils /// The repositioned hit objects. public static List RepositionHitObjects(IEnumerable objectPositionInfos) { - List positionInfos = objectPositionInfos.Select(o => new ObjectPositionInfoInternal(o)).ToList(); - ObjectPositionInfoInternal? previous = null; + List workingObjects = objectPositionInfos.Select(o => new WorkingObject(o)).ToList(); + WorkingObject? previous = null; - for (int i = 0; i < positionInfos.Count; i++) + for (int i = 0; i < workingObjects.Count; i++) { - var current = positionInfos[i]; + var current = workingObjects[i]; var hitObject = current.HitObject; if (hitObject is Spinner) @@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Utils continue; } - computeModifiedPosition(current, previous, i > 1 ? positionInfos[i - 2] : null); + computeModifiedPosition(current, previous, i > 1 ? workingObjects[i - 2] : null); // Move hit objects back into the playfield if they are outside of it Vector2 shift = Vector2.Zero; @@ -97,9 +97,9 @@ namespace osu.Game.Rulesets.Osu.Utils for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--) { // only shift hit circles - if (!(positionInfos[j].HitObject is HitCircle)) break; + if (!(workingObjects[j].HitObject is HitCircle)) break; - toBeShifted.Add(positionInfos[j].HitObject); + toBeShifted.Add(workingObjects[j].HitObject); } if (toBeShifted.Count > 0) @@ -109,16 +109,16 @@ namespace osu.Game.Rulesets.Osu.Utils previous = current; } - return positionInfos.Select(p => p.HitObject).ToList(); + return workingObjects.Select(p => p.HitObject).ToList(); } /// /// Compute the modified position of a hit object while attempting to keep it inside the playfield. /// - /// The representing the hit object to have the modified position computed for. - /// The representing the hit object immediately preceding the current one. - /// The representing the hit object immediately preceding the one. - private static void computeModifiedPosition(ObjectPositionInfoInternal current, ObjectPositionInfoInternal? previous, ObjectPositionInfoInternal? beforePrevious) + /// The representing the hit object to have the modified position computed for. + /// The representing the hit object immediately preceding the current one. + /// The representing the hit object immediately preceding the one. + private static void computeModifiedPosition(WorkingObject current, WorkingObject? previous, WorkingObject? beforePrevious) { float previousAbsoluteAngle = 0f; @@ -129,11 +129,11 @@ namespace osu.Game.Rulesets.Osu.Utils previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); } - float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle; + float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle; var posRelativeToPrev = new Vector2( - current.DistanceFromPrevious * (float)Math.Cos(absoluteAngle), - current.DistanceFromPrevious * (float)Math.Sin(absoluteAngle) + current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle), + current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle) ); Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre; @@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// Move the modified position of a hit circle so that it fits inside the playfield. /// /// The deviation from the original modified position in order to fit within the playfield. - private static Vector2 clampHitCircleToPlayfield(HitCircle circle, ObjectPositionInfoInternal objectPositionInfo) + private static Vector2 clampHitCircleToPlayfield(HitCircle circle, WorkingObject objectPositionInfo) { var previousPosition = objectPositionInfo.PositionModified; objectPositionInfo.EndPositionModified = objectPositionInfo.PositionModified = clampToPlayfieldWithPadding( @@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// Moves the and all necessary nested s into the if they aren't already. /// /// The deviation from the original modified position in order to fit within the playfield. - private static Vector2 clampSliderToPlayfield(Slider slider, ObjectPositionInfoInternal objectPositionInfo) + private static Vector2 clampSliderToPlayfield(Slider slider, WorkingObject objectPositionInfo) { var possibleMovementBounds = calculatePossibleMovementBounds(slider); @@ -319,17 +319,18 @@ namespace osu.Game.Rulesets.Osu.Utils } } - private class ObjectPositionInfoInternal : ObjectPositionInfo + private class WorkingObject { public Vector2 PositionOriginal { get; } public Vector2 PositionModified { get; set; } public Vector2 EndPositionModified { get; set; } - public ObjectPositionInfoInternal(ObjectPositionInfo original) - : base(original.HitObject) + public ObjectPositionInfo PositionInfo { get; } + public OsuHitObject HitObject => PositionInfo.HitObject; + + public WorkingObject(ObjectPositionInfo positionInfo) { - RelativeAngle = original.RelativeAngle; - DistanceFromPrevious = original.DistanceFromPrevious; + PositionInfo = positionInfo; PositionModified = PositionOriginal = HitObject.Position; EndPositionModified = HitObject.EndPosition; } From 76021c76278cf3a91a6f38e6b9b047a93f5dadad Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 14 Mar 2022 20:23:35 +0800 Subject: [PATCH 015/285] Remove extra parameters --- .../OsuHitObjectGenerationUtils_Reposition.cs | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index 94f4f154bd..d1bc3b45df 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -81,12 +81,12 @@ namespace osu.Game.Rulesets.Osu.Utils switch (hitObject) { - case HitCircle circle: - shift = clampHitCircleToPlayfield(circle, current); + case HitCircle _: + shift = clampHitCircleToPlayfield(current); break; - case Slider slider: - shift = clampSliderToPlayfield(slider, current); + case Slider _: + shift = clampSliderToPlayfield(current); break; } @@ -144,48 +144,49 @@ namespace osu.Game.Rulesets.Osu.Utils } /// - /// Move the modified position of a hit circle so that it fits inside the playfield. + /// Move the modified position of a so that it fits inside the playfield. /// /// The deviation from the original modified position in order to fit within the playfield. - private static Vector2 clampHitCircleToPlayfield(HitCircle circle, WorkingObject objectPositionInfo) + private static Vector2 clampHitCircleToPlayfield(WorkingObject workingObject) { - var previousPosition = objectPositionInfo.PositionModified; - objectPositionInfo.EndPositionModified = objectPositionInfo.PositionModified = clampToPlayfieldWithPadding( - objectPositionInfo.PositionModified, - (float)circle.Radius + var previousPosition = workingObject.PositionModified; + workingObject.EndPositionModified = workingObject.PositionModified = clampToPlayfieldWithPadding( + workingObject.PositionModified, + (float)workingObject.HitObject.Radius ); - circle.Position = objectPositionInfo.PositionModified; + workingObject.HitObject.Position = workingObject.PositionModified; - return objectPositionInfo.PositionModified - previousPosition; + return workingObject.PositionModified - previousPosition; } /// /// Moves the and all necessary nested s into the if they aren't already. /// /// The deviation from the original modified position in order to fit within the playfield. - private static Vector2 clampSliderToPlayfield(Slider slider, WorkingObject objectPositionInfo) + private static Vector2 clampSliderToPlayfield(WorkingObject workingObject) { + var slider = (Slider)workingObject.HitObject; var possibleMovementBounds = calculatePossibleMovementBounds(slider); - var previousPosition = objectPositionInfo.PositionModified; + var previousPosition = workingObject.PositionModified; // Clamp slider position to the placement area // If the slider is larger than the playfield, force it to stay at the original position float newX = possibleMovementBounds.Width < 0 - ? objectPositionInfo.PositionOriginal.X + ? workingObject.PositionOriginal.X : Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right); float newY = possibleMovementBounds.Height < 0 - ? objectPositionInfo.PositionOriginal.Y + ? workingObject.PositionOriginal.Y : Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom); - slider.Position = objectPositionInfo.PositionModified = new Vector2(newX, newY); - objectPositionInfo.EndPositionModified = slider.EndPosition; + slider.Position = workingObject.PositionModified = new Vector2(newX, newY); + workingObject.EndPositionModified = slider.EndPosition; - shiftNestedObjects(slider, objectPositionInfo.PositionModified - objectPositionInfo.PositionOriginal); + shiftNestedObjects(slider, workingObject.PositionModified - workingObject.PositionOriginal); - return objectPositionInfo.PositionModified - previousPosition; + return workingObject.PositionModified - previousPosition; } /// From 624f9fc774732ae8c7ee795968414d3f7f97926b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Mar 2022 22:41:28 +0100 Subject: [PATCH 016/285] Implement mod settings area component --- .../UserInterface/TestSceneModSettingsArea.cs | 40 ++++ osu.Game/Overlays/Mods/ModSettingsArea.cs | 176 ++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneModSettingsArea.cs create mode 100644 osu.Game/Overlays/Mods/ModSettingsArea.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettingsArea.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettingsArea.cs new file mode 100644 index 0000000000..ddc1c8c128 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettingsArea.cs @@ -0,0 +1,40 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestSceneModSettingsArea : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + + [Test] + public void TestModToggleArea() + { + ModSettingsArea modSettingsArea = null; + + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = modSettingsArea = new ModSettingsArea() + }); + AddStep("set DT", () => modSettingsArea.SelectedMods.Value = new[] { new OsuModDoubleTime() }); + AddStep("set DA", () => modSettingsArea.SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }); + AddStep("set FL+WU+DA+AD", () => modSettingsArea.SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() }); + AddStep("set empty", () => modSettingsArea.SelectedMods.Value = Array.Empty()); + } + } +} diff --git a/osu.Game/Overlays/Mods/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs new file mode 100644 index 0000000000..e0a30f60c2 --- /dev/null +++ b/osu.Game/Overlays/Mods/ModSettingsArea.cs @@ -0,0 +1,176 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + public class ModSettingsArea : CompositeDrawable + { + public Bindable> SelectedMods { get; } = new Bindable>(); + + private readonly Box background; + private readonly FillFlowContainer modSettingsFlow; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + + public ModSettingsArea() + { + RelativeSizeAxes = Axes.X; + Height = 250; + + Anchor = Anchor.BottomRight; + Origin = Anchor.BottomRight; + + InternalChild = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = 2, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new OsuScrollContainer(Direction.Horizontal) + { + RelativeSizeAxes = Axes.Both, + ScrollbarOverlapsContent = false, + Child = modSettingsFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Padding = new MarginPadding { Vertical = 7, Horizontal = 70 }, + Spacing = new Vector2(7), + Direction = FillDirection.Horizontal + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load() + { + background.Colour = colourProvider.Dark3; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + SelectedMods.BindValueChanged(_ => updateMods()); + } + + private void updateMods() + { + modSettingsFlow.Clear(); + + foreach (var mod in SelectedMods.Value.OrderBy(mod => mod.Type).ThenBy(mod => mod.Acronym)) + { + var settings = mod.CreateSettingsControls().ToList(); + + if (settings.Count > 0) + { + if (modSettingsFlow.Any()) + { + modSettingsFlow.Add(new Box + { + RelativeSizeAxes = Axes.Y, + Width = 2, + Colour = colourProvider.Dark4, + }); + } + + modSettingsFlow.Add(new ModSettingsColumn(mod, settings)); + } + } + } + + protected override bool OnMouseDown(MouseDownEvent e) => true; + protected override bool OnHover(HoverEvent e) => true; + + private class ModSettingsColumn : CompositeDrawable + { + public ModSettingsColumn(Mod mod, IEnumerable settingsControls) + { + Width = 250; + RelativeSizeAxes = Axes.Y; + Padding = new MarginPadding { Bottom = 7 }; + + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension() + }, + Content = new[] + { + new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(7), + Children = new Drawable[] + { + new ModSwitchTiny(mod) + { + Active = { Value = true }, + Scale = new Vector2(0.6f), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft + }, + new OsuSpriteText + { + Text = mod.Name, + Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Margin = new MarginPadding { Bottom = 2 } + } + } + } + }, + new[] { Empty() }, + new Drawable[] + { + new OsuScrollContainer(Direction.Vertical) + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Right = 7 }, + ChildrenEnumerable = settingsControls, + Spacing = new Vector2(0, 7) + } + } + } + } + }; + } + } + } +} From 4adb8c205f7523768f741c2b6d80f0aaa04fdf8a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Mar 2022 07:18:49 +0300 Subject: [PATCH 017/285] Add step to switch hosts randomly in `TestManyUsers` --- .../Multiplayer/TestSceneMultiplayerParticipantsList.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 292319171d..bc3bce4b3f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -202,9 +202,11 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestManyUsers() { + const int users_count = 20; + AddStep("add many users", () => { - for (int i = 0; i < 20; i++) + for (int i = 0; i < users_count; i++) { MultiplayerClient.AddUser(new APIUser { @@ -243,6 +245,8 @@ namespace osu.Game.Tests.Visual.Multiplayer } } }); + + AddRepeatStep("switch hosts", () => MultiplayerClient.TransferHost(RNG.Next(0, users_count)), 10); } [Test] From 1c899d00b92cdea921fcad9ea15cf33fbc839064 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Mar 2022 07:25:03 +0300 Subject: [PATCH 018/285] Pin multiplayer host panel to the top of the list --- .../Multiplayer/Participants/ParticipantsList.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs index afb2111023..6169ad6a9e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,6 +16,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { private FillFlowContainer panels; + [CanBeNull] + private ParticipantPanel currentHostPanel; + [BackgroundDependencyLoader] private void load() { @@ -55,6 +59,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants // Add panels for all users new to the room. foreach (var user in Room.Users.Except(panels.Select(p => p.User))) panels.Add(new ParticipantPanel(user)); + + if (currentHostPanel == null || !currentHostPanel.User.Equals(Room.Host)) + { + // Reset position of previous host back to normal, if one existing. + if (currentHostPanel != null && panels.Contains(currentHostPanel)) + panels.SetLayoutPosition(currentHostPanel, 0); + + // Change position of new host to display above all participants. + panels.SetLayoutPosition(currentHostPanel = panels.Single(u => u.User.Equals(Room.Host)), -1); + } } } } From 98b420ee6f2b256aa6e70847751fe311739b2572 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Mar 2022 07:25:10 +0300 Subject: [PATCH 019/285] Remove no longer correct crown fade animation Since the host is pinned to the top without any animation, it would look jarring for the crown to fade away from the old panel (and at a 50ms duration). --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 96a665f33d..11dba2e52d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -206,10 +206,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants else kickButton.FadeOut(fade_time); - if (Room.Host?.Equals(User) == true) - crown.FadeIn(fade_time); - else - crown.FadeOut(fade_time); + crown.Alpha = Room.Host?.Equals(User) == true ? 1 : 0; // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. From d0cc68bc97ade2891f3e1ee767588d0946b27507 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Mar 2022 07:26:10 +0300 Subject: [PATCH 020/285] Add test coverage --- .../TestSceneMultiplayerParticipantsList.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index bc3bce4b3f..50faa0a567 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -163,6 +163,25 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("second user crown visible", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 1); } + [Test] + public void TestHostGetsPinnedToTop() + { + AddStep("add user", () => MultiplayerClient.AddUser(new APIUser + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + })); + + AddStep("make second user host", () => MultiplayerClient.TransferHost(3)); + AddAssert("second user above first", () => + { + var first = this.ChildrenOfType().ElementAt(0); + var second = this.ChildrenOfType().ElementAt(1); + return second.Y < first.Y; + }); + } + [Test] public void TestKickButtonOnlyPresentWhenHost() { From a7ddfc7f519d2b3583814425071eccf1d4c8d5d2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Mar 2022 08:08:31 +0300 Subject: [PATCH 021/285] Add step for returning host back to local user --- .../Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 50faa0a567..8da077cd44 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -266,6 +266,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddRepeatStep("switch hosts", () => MultiplayerClient.TransferHost(RNG.Next(0, users_count)), 10); + AddStep("give host back", () => MultiplayerClient.TransferHost(API.LocalUser.Value.Id)); } [Test] From 1bd08b4a4b02499a7aed551d338494d3dd0952f6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Mar 2022 08:08:45 +0300 Subject: [PATCH 022/285] Remove kick button fading as well to not look jarring --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 11dba2e52d..e95ca4de32 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -201,11 +201,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants else userModsDisplay.FadeOut(fade_time); - if (Client.IsHost && !User.Equals(Client.LocalUser)) - kickButton.FadeIn(fade_time); - else - kickButton.FadeOut(fade_time); - + kickButton.Alpha = Client.IsHost && !User.Equals(Client.LocalUser) ? 1 : 0; crown.Alpha = Room.Host?.Equals(User) == true ? 1 : 0; // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 From b76a87e6f886401cdc34245e41b706f4a8d6595f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Mar 2022 18:58:09 +0900 Subject: [PATCH 023/285] Split ready button visual logic into button itself --- .../Visual/Multiplayer/QueueModeTestScene.cs | 4 +- .../TestSceneAllPlayersQueueMode.cs | 8 +- .../TestSceneMultiplayerMatchSubScreen.cs | 2 +- .../TestSceneMultiplayerReadyButton.cs | 16 +- .../Match/MultiplayerReadyButton.cs | 172 ++++++++++++------ 5 files changed, 132 insertions(+), 70 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index bafc579134..df8f63f6ed 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -95,10 +95,10 @@ namespace osu.Game.Tests.Visual.Multiplayer protected void RunGameplay() { AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded); AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs index 0785315b26..266ac60168 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs @@ -102,10 +102,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); AddAssert("ruleset is correct", () => ((Player)CurrentScreen).Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); @@ -119,10 +119,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); AddAssert("mods are correct", () => !((Player)CurrentScreen).Mods.Value.Any()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 057032c413..a51d4678fe 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for spectating user state", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("match started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index f34f7c6c91..7f7a4c9c5f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -92,10 +92,10 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); } @@ -111,7 +111,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); verifyGameplayStartFlow(); @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0)); verifyGameplayStartFlow(); @@ -141,12 +141,12 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0)); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); } @@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer if (!isHost) AddStep("transfer host", () => MultiplayerClient.TransferHost(2)); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddRepeatStep("change user ready state", () => { @@ -184,7 +184,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void verifyGameplayStartFlow() { AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); AddStep("finish gameplay", () => diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 0c80f6ef5b..920e23eaa1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -14,16 +14,12 @@ using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Online.Multiplayer; -using osu.Game.Screens.OnlinePlay.Components; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public class MultiplayerReadyButton : MultiplayerRoomComposite { - [Resolved] - private OsuColour colours { get; set; } - [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } @@ -34,14 +30,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private Sample sampleReadyAll; private Sample sampleUnready; - private readonly ButtonWithTrianglesExposed button; + private readonly ReadyButton readyButton; private int countReady; private ScheduledDelegate readySampleDelegate; private IBindable operationInProgress; public MultiplayerReadyButton() { - InternalChild = button = new ButtonWithTrianglesExposed + InternalChild = readyButton = new ReadyButton { RelativeSizeAxes = Axes.Both, Size = Vector2.One, @@ -123,47 +119,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void updateState() { - var localUser = Client.LocalUser; - - int newCountReady = Room?.Users.Count(u => u.State == MultiplayerUserState.Ready) ?? 0; - int newCountTotal = Room?.Users.Count(u => u.State != MultiplayerUserState.Spectating) ?? 0; - - switch (localUser?.State) + if (Room == null) { - default: - button.Text = "Ready"; - updateButtonColour(true); - break; - - case MultiplayerUserState.Spectating: - case MultiplayerUserState.Ready: - string countText = $"({newCountReady} / {newCountTotal} ready)"; - - if (Room?.Host?.Equals(localUser) == true) - { - button.Text = $"Start match {countText}"; - updateButtonColour(true); - } - else - { - button.Text = $"Waiting for host... {countText}"; - updateButtonColour(false); - } - - break; + readyButton.Enabled.Value = false; + return; } - bool enableButton = - Room?.State == MultiplayerRoomState.Open + var localUser = Client.LocalUser; + + int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); + int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating); + + readyButton.Enabled.Value = + Room.State == MultiplayerRoomState.Open && CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId && !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired && !operationInProgress.Value; // When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready. if (localUser?.State == MultiplayerUserState.Spectating) - enableButton &= Room?.Host?.Equals(localUser) == true && newCountReady > 0; - - button.Enabled.Value = enableButton; + readyButton.Enabled.Value &= Room.Host?.Equals(localUser) == true && newCountReady > 0; if (newCountReady == countReady) return; @@ -187,25 +162,112 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }); } - private void updateButtonColour(bool green) - { - if (green) - { - button.BackgroundColour = colours.Green; - button.Triangles.ColourDark = colours.Green; - button.Triangles.ColourLight = colours.GreenLight; - } - else - { - button.BackgroundColour = colours.YellowDark; - button.Triangles.ColourDark = colours.YellowDark; - button.Triangles.ColourLight = colours.Yellow; - } - } - - private class ButtonWithTrianglesExposed : ReadyButton + public class ReadyButton : Components.ReadyButton { public new Triangles Triangles => base.Triangles; + + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + + [CanBeNull] + private MultiplayerRoom room => multiplayerClient.Room; + + protected override void LoadComplete() + { + base.LoadComplete(); + + multiplayerClient.RoomUpdated += () => Scheduler.AddOnce(onRoomUpdated); + onRoomUpdated(); + } + + private void onRoomUpdated() + { + updateButtonText(); + updateButtonColour(); + } + + private void updateButtonText() + { + if (room == null) + { + Text = "Ready"; + return; + } + + var localUser = multiplayerClient.LocalUser; + + int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready); + int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating); + + string countText = $"({countReady} / {countTotal} ready)"; + + switch (localUser?.State) + { + default: + Text = "Ready"; + break; + + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + Text = room.Host?.Equals(localUser) == true + ? $"Start match {countText}" + : $"Waiting for host... {countText}"; + + break; + } + } + + private void updateButtonColour() + { + if (room == null) + { + setGreen(); + return; + } + + var localUser = multiplayerClient.LocalUser; + + switch (localUser?.State) + { + default: + setGreen(); + break; + + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + if (room?.Host?.Equals(localUser) == true) + setGreen(); + else + setYellow(); + + break; + } + + void setYellow() + { + BackgroundColour = colours.YellowDark; + Triangles.ColourDark = colours.YellowDark; + Triangles.ColourLight = colours.Yellow; + } + + void setGreen() + { + BackgroundColour = colours.Green; + Triangles.ColourDark = colours.Green; + Triangles.ColourLight = colours.GreenLight; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (multiplayerClient != null) + multiplayerClient.RoomUpdated -= onRoomUpdated; + } } } } From efce471f0baacc69239aecdea135faf99586f974 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Mar 2022 19:05:28 +0900 Subject: [PATCH 024/285] Add countdown button + popover --- .../TestSceneMultiplayerReadyButton.cs | 18 +- .../TestSceneMultiplayerSpectateButton.cs | 33 ++-- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 181 +++++++++--------- .../Match/MultiplayerReadyButton.cs | 132 ++++++++++++- 4 files changed, 246 insertions(+), 118 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 7f7a4c9c5f..12b3b90fc4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -8,6 +8,7 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Utils; @@ -55,15 +56,16 @@ namespace osu.Game.Tests.Visual.Multiplayer RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID }; - if (button != null) - Remove(button); - - Add(button = new MultiplayerReadyButton + Child = new PopoverContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(200, 50), - }); + RelativeSizeAxes = Axes.Both, + Child = button = new MultiplayerReadyButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 50), + } + }; }); [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 33ad0fd1de..13c9e021db 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -56,23 +57,27 @@ namespace osu.Game.Tests.Visual.Multiplayer RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, }; - Child = new FillFlowContainer + Child = new PopoverContainer { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer { - spectateButton = new MultiplayerSpectateButton + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(200, 50), - }, - readyButton = new MultiplayerReadyButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(200, 50), + spectateButton = new MultiplayerSpectateButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 50), + }, + readyButton = new MultiplayerReadyButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 50), + } } } }; diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index e297c90491..a382f65d84 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; using osu.Game.Audio; @@ -100,122 +101,126 @@ namespace osu.Game.Screens.OnlinePlay.Match { sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); - InternalChildren = new Drawable[] + InternalChild = new PopoverContainer { - beatmapAvailabilityTracker, - new MultiplayerRoomSounds(), - new GridContainer + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + beatmapAvailabilityTracker, + new MultiplayerRoomSounds(), + new GridContainer { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 50) - }, - Content = new[] - { - // Padded main content (drawable room + main content) - new Drawable[] + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - new Container + new Dimension(), + new Dimension(GridSizeMode.Absolute, 50) + }, + Content = new[] + { + // Padded main content (drawable room + main content) + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + new Container { - Horizontal = WaveOverlayContainer.WIDTH_PADDING, - Bottom = 30 - }, - Children = new[] - { - mainContent = new GridContainer + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Horizontal = WaveOverlayContainer.WIDTH_PADDING, + Bottom = 30 + }, + Children = new[] + { + mainContent = new GridContainer { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, 10) - }, - Content = new[] - { - new Drawable[] + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - new DrawableMatchRoom(Room, allowEdit) - { - OnEdit = () => settingsOverlay.Show(), - SelectedItem = { BindTarget = SelectedItem } - } + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 10) }, - null, - new Drawable[] + Content = new[] { - new Container + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Children = new[] + new DrawableMatchRoom(Room, allowEdit) { - new Container + OnEdit = () => settingsOverlay.Show(), + SelectedItem = { BindTarget = SelectedItem } + } + }, + null, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - Child = new Box + new Container { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + Masking = true, + CornerRadius = 10, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + }, }, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(20), - Child = CreateMainContent(), - }, - new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = userModsSelectOverlay = new UserModSelectOverlay + new Container { - SelectedMods = { BindTarget = UserMods }, - IsValidMod = _ => false - } - }, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(20), + Child = CreateMainContent(), + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = userModsSelectOverlay = new UserModSelectOverlay + { + SelectedMods = { BindTarget = UserMods }, + IsValidMod = _ => false + } + }, + } } } } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + // Resolves 1px masking errors between the settings overlay and the room panel. + Padding = new MarginPadding(-1), + Child = settingsOverlay = CreateRoomSettingsOverlay(Room) } }, - new Container - { - RelativeSizeAxes = Axes.Both, - // Resolves 1px masking errors between the settings overlay and the room panel. - Padding = new MarginPadding(-1), - Child = settingsOverlay = CreateRoomSettingsOverlay(Room) - } }, }, - }, - // Footer - new Drawable[] - { - new Container + // Footer + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + new Container { - new Box + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d") // Temporary. - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(5), - Child = CreateFooter() - }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d") // Temporary. + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(5), + Child = CreateFooter() + }, + } } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 920e23eaa1..4e53b40075 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -4,15 +4,24 @@ using System; using System.Diagnostics; using System.Linq; +using Humanizer; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.Multiplayer; using osuTK; @@ -30,19 +39,43 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private Sample sampleReadyAll; private Sample sampleUnready; - private readonly ReadyButton readyButton; + private readonly BindableBool enabled = new BindableBool(); + private readonly CountdownButton countdownButton; private int countReady; private ScheduledDelegate readySampleDelegate; private IBindable operationInProgress; public MultiplayerReadyButton() { - InternalChild = readyButton = new ReadyButton + InternalChild = new GridContainer { RelativeSizeAxes = Axes.Both, - Size = Vector2.One, - Action = onReadyClick, - Enabled = { Value = true }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new ReadyButton + { + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + Action = onReadyClick, + Enabled = { BindTarget = enabled }, + }, + countdownButton = new CountdownButton + { + RelativeSizeAxes = Axes.Y, + Size = new Vector2(40, 1), + Alpha = 0, + Action = startCountdown, + Enabled = { BindTarget = enabled } + } + } + } }; } @@ -111,6 +144,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }); } + private void startCountdown(TimeSpan duration) + { + } + private void endOperation() { clickOperation?.Dispose(); @@ -121,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { if (Room == null) { - readyButton.Enabled.Value = false; + enabled.Value = false; return; } @@ -130,7 +167,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating); - readyButton.Enabled.Value = + switch (localUser?.State) + { + default: + countdownButton.Alpha = 0; + break; + + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0; + break; + } + + enabled.Value = Room.State == MultiplayerRoomState.Open && CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId && !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired @@ -138,7 +187,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match // When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready. if (localUser?.State == MultiplayerUserState.Spectating) - readyButton.Enabled.Value &= Room.Host?.Equals(localUser) == true && newCountReady > 0; + enabled.Value &= Room.Host?.Equals(localUser) == true && newCountReady > 0; if (newCountReady == countReady) return; @@ -269,5 +318,72 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match multiplayerClient.RoomUpdated -= onRoomUpdated; } } + + public class CountdownButton : IconButton, IHasPopover + { + private static readonly TimeSpan[] available_delays = + { + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30), + TimeSpan.FromMinutes(1), + TimeSpan.FromMinutes(2) + }; + + public new Action Action; + + private readonly Drawable background; + + public CountdownButton() + { + Icon = FontAwesome.Solid.CaretDown; + IconScale = new Vector2(0.6f); + + Add(background = new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }); + + base.Action = this.ShowPopover; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = colours.Green; + } + + public Popover GetPopover() + { + var flow = new FillFlowContainer + { + Width = 200, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + }; + + foreach (var duration in available_delays) + { + flow.Add(new PopoverButton + { + RelativeSizeAxes = Axes.X, + Text = $"Start match in {duration.Humanize()}", + BackgroundColour = background.Colour, + Action = () => + { + Action(duration); + this.HidePopover(); + } + }); + } + + return new OsuPopover { Child = flow }; + } + + public class PopoverButton : OsuButton + { + } + } } } From 3b938865a1d3fd4ee4d9972dd37a36f2365ae7df Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Mar 2022 19:14:46 +0900 Subject: [PATCH 025/285] Add room structure for countdown timers --- .../Countdown/CountdownChangedEvent.cs | 22 +++++++++++++++++ .../Countdown/MatchStartCountdownRequest.cs | 23 ++++++++++++++++++ .../Countdown/StopCountdownRequest.cs | 17 +++++++++++++ .../Online/Multiplayer/MatchServerEvent.cs | 5 ++++ .../Online/Multiplayer/MatchStartCountdown.cs | 17 +++++++++++++ .../TeamVersus/ChangeTeamRequest.cs | 1 + .../Online/Multiplayer/MatchUserRequest.cs | 6 ++++- .../Multiplayer/MultiplayerCountdown.cs | 24 +++++++++++++++++++ .../Online/Multiplayer/MultiplayerRoom.cs | 6 +++++ osu.Game/Online/SignalRWorkaroundTypes.cs | 5 ++++ 10 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Online/Multiplayer/Countdown/CountdownChangedEvent.cs create mode 100644 osu.Game/Online/Multiplayer/Countdown/MatchStartCountdownRequest.cs create mode 100644 osu.Game/Online/Multiplayer/Countdown/StopCountdownRequest.cs create mode 100644 osu.Game/Online/Multiplayer/MatchStartCountdown.cs create mode 100644 osu.Game/Online/Multiplayer/MultiplayerCountdown.cs diff --git a/osu.Game/Online/Multiplayer/Countdown/CountdownChangedEvent.cs b/osu.Game/Online/Multiplayer/Countdown/CountdownChangedEvent.cs new file mode 100644 index 0000000000..b067f3b235 --- /dev/null +++ b/osu.Game/Online/Multiplayer/Countdown/CountdownChangedEvent.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using MessagePack; + +namespace osu.Game.Online.Multiplayer.Countdown +{ + /// + /// Indicates a change to the 's countdown. + /// + [MessagePackObject] + public class CountdownChangedEvent : MatchServerEvent + { + /// + /// The new countdown. + /// + [Key(0)] + public MultiplayerCountdown? Countdown { get; set; } + } +} diff --git a/osu.Game/Online/Multiplayer/Countdown/MatchStartCountdownRequest.cs b/osu.Game/Online/Multiplayer/Countdown/MatchStartCountdownRequest.cs new file mode 100644 index 0000000000..04e7f506c2 --- /dev/null +++ b/osu.Game/Online/Multiplayer/Countdown/MatchStartCountdownRequest.cs @@ -0,0 +1,23 @@ +// 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 MessagePack; + +#nullable enable + +namespace osu.Game.Online.Multiplayer.Countdown +{ + /// + /// A request for a countdown to start the match. + /// + [MessagePackObject] + public class MatchStartCountdownRequest : MatchUserRequest + { + /// + /// How long the countdown should last. + /// + [Key(0)] + public TimeSpan Delay { get; set; } + } +} diff --git a/osu.Game/Online/Multiplayer/Countdown/StopCountdownRequest.cs b/osu.Game/Online/Multiplayer/Countdown/StopCountdownRequest.cs new file mode 100644 index 0000000000..20a0e32734 --- /dev/null +++ b/osu.Game/Online/Multiplayer/Countdown/StopCountdownRequest.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using MessagePack; + +namespace osu.Game.Online.Multiplayer.Countdown +{ + /// + /// Request to stop the current countdown. + /// + [MessagePackObject] + public class StopCountdownRequest : MatchUserRequest + { + } +} diff --git a/osu.Game/Online/Multiplayer/MatchServerEvent.cs b/osu.Game/Online/Multiplayer/MatchServerEvent.cs index 891fb2cc3b..4ce55e424d 100644 --- a/osu.Game/Online/Multiplayer/MatchServerEvent.cs +++ b/osu.Game/Online/Multiplayer/MatchServerEvent.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using MessagePack; +using osu.Game.Online.Multiplayer.Countdown; namespace osu.Game.Online.Multiplayer { @@ -11,6 +14,8 @@ namespace osu.Game.Online.Multiplayer /// [Serializable] [MessagePackObject] + // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. + [Union(0, typeof(CountdownChangedEvent))] public abstract class MatchServerEvent { } diff --git a/osu.Game/Online/Multiplayer/MatchStartCountdown.cs b/osu.Game/Online/Multiplayer/MatchStartCountdown.cs new file mode 100644 index 0000000000..6c1cdd97d3 --- /dev/null +++ b/osu.Game/Online/Multiplayer/MatchStartCountdown.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using MessagePack; + +namespace osu.Game.Online.Multiplayer +{ + /// + /// A which will start the match after ending. + /// + [MessagePackObject] + public class MatchStartCountdown : MultiplayerCountdown + { + } +} diff --git a/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/ChangeTeamRequest.cs b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/ChangeTeamRequest.cs index 9c3b07049c..a26a2b3fc2 100644 --- a/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/ChangeTeamRequest.cs +++ b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/ChangeTeamRequest.cs @@ -7,6 +7,7 @@ using MessagePack; namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus { + [MessagePackObject] public class ChangeTeamRequest : MatchUserRequest { [Key(0)] diff --git a/osu.Game/Online/Multiplayer/MatchUserRequest.cs b/osu.Game/Online/Multiplayer/MatchUserRequest.cs index 8c6809e7f3..fa7bdd8afe 100644 --- a/osu.Game/Online/Multiplayer/MatchUserRequest.cs +++ b/osu.Game/Online/Multiplayer/MatchUserRequest.cs @@ -3,6 +3,7 @@ using System; using MessagePack; +using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; namespace osu.Game.Online.Multiplayer @@ -12,7 +13,10 @@ namespace osu.Game.Online.Multiplayer /// [Serializable] [MessagePackObject] - [Union(0, typeof(ChangeTeamRequest))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. + // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. + [Union(0, typeof(ChangeTeamRequest))] + [Union(1, typeof(MatchStartCountdownRequest))] + [Union(2, typeof(StopCountdownRequest))] public abstract class MatchUserRequest { } diff --git a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs new file mode 100644 index 0000000000..63bb47b295 --- /dev/null +++ b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using MessagePack; + +namespace osu.Game.Online.Multiplayer +{ + /// + /// Describes the current countdown in a . + /// + [MessagePackObject] + [Union(0, typeof(MatchStartCountdown))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. + public abstract class MultiplayerCountdown + { + /// + /// The time at which the countdown will end. + /// + [Key(0)] + public DateTimeOffset EndTime { get; set; } + } +} diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs index a60e70dab3..e215498ff9 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -54,6 +54,12 @@ namespace osu.Game.Online.Multiplayer [Key(6)] public IList Playlist { get; set; } = new List(); + /// + /// The currently-running countdown. + /// + [Key(7)] + public MultiplayerCountdown? Countdown { get; set; } + [JsonConstructor] [SerializationConstructor] public MultiplayerRoom(long roomId) diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index f69d23d81c..3e2b697337 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; namespace osu.Game.Online @@ -18,8 +19,12 @@ namespace osu.Game.Online internal static readonly IReadOnlyList<(Type derivedType, Type baseType)> BASE_TYPE_MAPPING = new[] { (typeof(ChangeTeamRequest), typeof(MatchUserRequest)), + (typeof(MatchStartCountdownRequest), typeof(MatchUserRequest)), + (typeof(StopCountdownRequest), typeof(MatchUserRequest)), + (typeof(CountdownChangedEvent), typeof(MatchServerEvent)), (typeof(TeamVersusRoomState), typeof(MatchRoomState)), (typeof(TeamVersusUserState), typeof(MatchUserState)), + (typeof(MatchStartCountdown), typeof(MultiplayerCountdown)) }; } } From 72843a679783d6c762117398f36c6dfc26a71d2a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Mar 2022 19:26:42 +0900 Subject: [PATCH 026/285] Add support for starting/stopping countdowns --- .../TestSceneMultiplayerReadyButton.cs | 136 ++++++++++++++++++ .../Online/Multiplayer/MultiplayerClient.cs | 20 ++- .../OnlinePlay/Components/ReadyButton.cs | 12 +- .../Match/MultiplayerReadyButton.cs | 136 ++++++++++++++---- .../Multiplayer/TestMultiplayerClient.cs | 89 +++++++++++- 5 files changed, 354 insertions(+), 39 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 12b3b90fc4..b4cbc40403 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -16,11 +17,13 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Resources; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { @@ -68,6 +71,139 @@ namespace osu.Game.Tests.Visual.Multiplayer }; }); + [Test] + public void TestStartWithCountdown() + { + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); + AddStep("click the first countdown button", () => + { + var popoverButton = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(popoverButton); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); + AddStep("finish countdown", () => MultiplayerClient.FinishCountDown()); + AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); + } + + [Test] + public void TestCancelCountdown() + { + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); + AddStep("click the first countdown button", () => + { + var popoverButton = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(popoverButton); + InputManager.Click(MouseButton.Left); + }); + + ClickButtonWhenEnabled(); + + AddStep("finish countdown", () => MultiplayerClient.FinishCountDown()); + AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); + } + + [Test] + public void TestReadyAndUnReadyDuringCountdown() + { + AddStep("add second user as host", () => + { + MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); + MultiplayerClient.TransferHost(2); + }); + + AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new MatchStartCountdownRequest { Delay = TimeSpan.FromMinutes(2) })); + + ClickButtonWhenEnabled(); + AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); + + ClickButtonWhenEnabled(); + AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); + } + + [Test] + public void TestCountdownButtonEnablementAndVisibilityWhileSpectating() + { + AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); + AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); + + AddAssert("countdown button is visible", () => this.ChildrenOfType().Single().IsPresent); + AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); + AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); + AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + } + + [Test] + public void TestSpectatingDuringCountdownWithNoReadyUsersCancelsCountdown() + { + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); + AddStep("click the first countdown button", () => + { + var popoverButton = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(popoverButton); + InputManager.Click(MouseButton.Left); + }); + + AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); + AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); + + AddStep("finish countdown", () => MultiplayerClient.FinishCountDown()); + AddUntilStep("match not started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.Open); + } + + [Test] + public void TestReadyButtonEnabledWhileSpectatingDuringCountdown() + { + AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); + AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); + + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); + AddStep("click the first countdown button", () => + { + var popoverButton = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(popoverButton); + InputManager.Click(MouseButton.Left); + }); + + AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); + AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); + + AddAssert("ready button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + } + + [Test] + public void TestBecomeHostDuringCountdownAndReady() + { + AddStep("add second user as host", () => + { + MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); + MultiplayerClient.TransferHost(2); + }); + + AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new MatchStartCountdownRequest { Delay = TimeSpan.FromMinutes(1) })); + AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null); + + AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID)); + AddUntilStep("local user is host", () => MultiplayerClient.Room?.Host?.Equals(MultiplayerClient.LocalUser) == true); + + ClickButtonWhenEnabled(); + AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); + AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null); + } + [Test] public void TestDeletedBeatmapDisableReady() { diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index a56cc7f8d6..2d5496c5c1 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -16,6 +16,7 @@ using osu.Framework.Logging; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Rulesets; @@ -534,7 +535,24 @@ namespace osu.Game.Online.Multiplayer public Task MatchEvent(MatchServerEvent e) { - // not used by any match types just yet. + if (Room == null) + return Task.CompletedTask; + + Scheduler.Add(() => + { + if (Room == null) + return; + + switch (e) + { + case CountdownChangedEvent countdownChangedEvent: + Room.Countdown = countdownChangedEvent.Countdown; + break; + } + + RoomUpdated?.Invoke(); + }, false); + return Task.CompletedTask; } diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 9822ceaaf6..79cf5c7236 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -14,20 +14,18 @@ namespace osu.Game.Screens.OnlinePlay.Components public abstract class ReadyButton : TriangleButton, IHasTooltip { public new readonly BindableBool Enabled = new BindableBool(); - - private IBindable availability; + protected readonly IBindable Availability = new Bindable(); [BackgroundDependencyLoader] private void load(OnlinePlayBeatmapAvailabilityTracker beatmapTracker) { - availability = beatmapTracker.Availability.GetBoundCopy(); - - availability.BindValueChanged(_ => updateState()); + Availability.BindTo(beatmapTracker.Availability); + Availability.BindValueChanged(_ => updateState()); Enabled.BindValueChanged(_ => updateState(), true); } private void updateState() => - base.Enabled.Value = availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value; + base.Enabled.Value = Availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value; public virtual LocalisableString TooltipText { @@ -36,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Components if (Enabled.Value) return string.Empty; - if (availability.Value.State != DownloadState.LocallyAvailable) + if (Availability.Value.State != DownloadState.LocallyAvailable) return "Beatmap not downloaded"; return string.Empty; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 4e53b40075..f9f070f17a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -17,12 +17,14 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.Countdown; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match @@ -124,6 +126,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match return; } + // Local user is the room host and is in a ready state. + // The only action they can take is to stop a countdown if one's currently running. + if (Room.Countdown != null) + { + stopCountdown(); + return; + } + // And if a countdown isn't running, start the match. startMatch(); @@ -131,6 +141,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation()); + void stopCountdown() => Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation()); + void startMatch() => Client.StartMatch().ContinueWith(t => { // accessing Exception here silences any potential errors from the antecedent task @@ -146,6 +158,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void startCountdown(TimeSpan duration) { + Debug.Assert(clickOperation == null); + clickOperation = ongoingOperationTracker.BeginOperation(); + + Client.SendMatchRequest(new MatchStartCountdownRequest { Delay = duration }).ContinueWith(_ => endOperation()); } private void endOperation() @@ -167,16 +183,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating); - switch (localUser?.State) + if (Room.Countdown != null) + countdownButton.Alpha = 0; + else { - default: - countdownButton.Alpha = 0; - break; + switch (localUser?.State) + { + default: + countdownButton.Alpha = 0; + break; - case MultiplayerUserState.Spectating: - case MultiplayerUserState.Ready: - countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0; - break; + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0; + break; + } } enabled.Value = @@ -232,6 +253,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match onRoomUpdated(); } + protected override void Update() + { + base.Update(); + + if (room?.Countdown != null) + { + // Update the countdown timer. + onRoomUpdated(); + } + } + private void onRoomUpdated() { updateButtonText(); @@ -251,21 +283,39 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready); int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating); + string countdownText = room.Countdown == null ? string.Empty : $"Starting in {room.Countdown.EndTime - DateTimeOffset.Now:mm\\:ss}"; string countText = $"({countReady} / {countTotal} ready)"; - switch (localUser?.State) + if (room.Countdown != null) { - default: - Text = "Ready"; - break; + switch (localUser?.State) + { + default: + Text = $"Ready ({countdownText.ToLowerInvariant()})"; + break; - case MultiplayerUserState.Spectating: - case MultiplayerUserState.Ready: - Text = room.Host?.Equals(localUser) == true - ? $"Start match {countText}" - : $"Waiting for host... {countText}"; + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + Text = $"{countdownText} {countText}"; + break; + } + } + else + { + switch (localUser?.State) + { + default: + Text = "Ready"; + break; - break; + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + Text = room.Host?.Equals(localUser) == true + ? $"Start match {countText}" + : $"Waiting for host... {countText}"; + + break; + } } } @@ -279,20 +329,37 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match var localUser = multiplayerClient.LocalUser; - switch (localUser?.State) + if (room.Countdown != null) { - default: - setGreen(); - break; - - case MultiplayerUserState.Spectating: - case MultiplayerUserState.Ready: - if (room?.Host?.Equals(localUser) == true) + switch (localUser?.State) + { + default: setGreen(); - else - setYellow(); + break; - break; + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + setYellow(); + break; + } + } + else + { + switch (localUser?.State) + { + default: + setGreen(); + break; + + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + if (room?.Host?.Equals(localUser) == true) + setGreen(); + else + setYellow(); + + break; + } } void setYellow() @@ -317,6 +384,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (multiplayerClient != null) multiplayerClient.RoomUpdated -= onRoomUpdated; } + + public override LocalisableString TooltipText + { + get + { + if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready) + return "Cancel countdown"; + + return base.TooltipText; + } + } } public class CountdownButton : IconButton, IHasPopover diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 6dc5159b6f..a1ae1aa171 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; @@ -114,12 +116,24 @@ namespace osu.Game.Tests.Visual.Multiplayer public void ChangeUserState(int userId, MultiplayerUserState newState) { Debug.Assert(Room != null); + ((IMultiplayerClient)this).UserStateChanged(userId, newState); Schedule(() => { switch (Room.State) { + case MultiplayerRoomState.Open: + // If there are no remaining ready users or the host is not ready, stop any existing countdown. + // Todo: When we have an "automatic start" mode, this should also start a new countdown if any users _are_ ready. + // Todo: This doesn't yet support non-match-start countdowns. + bool shouldStopCountdown = Room.Users.All(u => u.State != MultiplayerUserState.Ready); + shouldStopCountdown |= Room.Host?.State != MultiplayerUserState.Ready && Room.Host?.State != MultiplayerUserState.Spectating; + + if (shouldStopCountdown) + countdownStopSource?.Cancel(); + break; + case MultiplayerRoomState.WaitingForLoad: if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad)) { @@ -282,6 +296,12 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } + private CancellationTokenSource? countdownFinishSource; + private CancellationTokenSource? countdownStopSource; + private Task countdownTask = Task.CompletedTask; + + public void FinishCountDown() => countdownFinishSource?.Cancel(); + public override async Task SendMatchRequest(MatchUserRequest request) { Debug.Assert(Room != null); @@ -289,6 +309,71 @@ namespace osu.Game.Tests.Visual.Multiplayer switch (request) { + case MatchStartCountdownRequest matchCountdownRequest: + countdownStopSource?.Cancel(); + + var stopSource = countdownStopSource = new CancellationTokenSource(); + var finishSource = countdownFinishSource = new CancellationTokenSource(); + var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, finishSource.Token); + var countdown = new MatchStartCountdown { EndTime = DateTimeOffset.Now + matchCountdownRequest.Delay }; + + Task lastCountdownTask = countdownTask; + countdownTask = start(); + + async Task start() + { + try + { + await lastCountdownTask; + } + catch (OperationCanceledException) + { + } + + Schedule(() => + { + if (stopSource.IsCancellationRequested) + return; + + Room.Countdown = countdown; + MatchEvent(new CountdownChangedEvent { Countdown = countdown }); + }); + + try + { + await Task.Delay(matchCountdownRequest.Delay, cancellationSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + + Schedule(() => + { + if (Room.Countdown != countdown) + return; + + Room.Countdown = null; + MatchEvent(new CountdownChangedEvent { Countdown = null }); + + using (cancellationSource) + { + if (stopSource.Token.IsCancellationRequested) + return; + } + + StartMatch().WaitSafely(); + }); + } + + break; + + case StopCountdownRequest _: + countdownStopSource?.Cancel(); + + Room.Countdown = null; + await MatchEvent(new CountdownChangedEvent { Countdown = Room.Countdown }); + break; + case ChangeTeamRequest changeTeam: TeamVersusRoomState roomState = (TeamVersusRoomState)Room.MatchState!; @@ -307,7 +392,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } } - public override Task StartMatch() + public override async Task StartMatch() { Debug.Assert(Room != null); @@ -315,7 +400,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad); - return ((IMultiplayerClient)this).LoadRequested(); + await ((IMultiplayerClient)this).LoadRequested(); } public override Task AbortGameplay() From 04f4e81852b846caee2b70e9cee1b47ac64dd7bc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 18 Mar 2022 21:05:19 +0900 Subject: [PATCH 027/285] Rename start countdown request --- .../Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs | 4 ++-- ...StartCountdownRequest.cs => StartMatchCountdownRequest.cs} | 2 +- osu.Game/Online/Multiplayer/MatchUserRequest.cs | 2 +- osu.Game/Online/SignalRWorkaroundTypes.cs | 2 +- .../OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs | 2 +- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/Online/Multiplayer/Countdown/{MatchStartCountdownRequest.cs => StartMatchCountdownRequest.cs} (89%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index b4cbc40403..fc47861576 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new MatchStartCountdownRequest { Delay = TimeSpan.FromMinutes(2) })); + AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(2) })); ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); @@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new MatchStartCountdownRequest { Delay = TimeSpan.FromMinutes(1) })); + AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(1) })); AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null); AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID)); diff --git a/osu.Game/Online/Multiplayer/Countdown/MatchStartCountdownRequest.cs b/osu.Game/Online/Multiplayer/Countdown/StartMatchCountdownRequest.cs similarity index 89% rename from osu.Game/Online/Multiplayer/Countdown/MatchStartCountdownRequest.cs rename to osu.Game/Online/Multiplayer/Countdown/StartMatchCountdownRequest.cs index 04e7f506c2..9e6967af9d 100644 --- a/osu.Game/Online/Multiplayer/Countdown/MatchStartCountdownRequest.cs +++ b/osu.Game/Online/Multiplayer/Countdown/StartMatchCountdownRequest.cs @@ -12,7 +12,7 @@ namespace osu.Game.Online.Multiplayer.Countdown /// A request for a countdown to start the match. /// [MessagePackObject] - public class MatchStartCountdownRequest : MatchUserRequest + public class StartMatchCountdownRequest : MatchUserRequest { /// /// How long the countdown should last. diff --git a/osu.Game/Online/Multiplayer/MatchUserRequest.cs b/osu.Game/Online/Multiplayer/MatchUserRequest.cs index fa7bdd8afe..888b55e428 100644 --- a/osu.Game/Online/Multiplayer/MatchUserRequest.cs +++ b/osu.Game/Online/Multiplayer/MatchUserRequest.cs @@ -15,7 +15,7 @@ namespace osu.Game.Online.Multiplayer [MessagePackObject] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. [Union(0, typeof(ChangeTeamRequest))] - [Union(1, typeof(MatchStartCountdownRequest))] + [Union(1, typeof(StartMatchCountdownRequest))] [Union(2, typeof(StopCountdownRequest))] public abstract class MatchUserRequest { diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index 3e2b697337..156f916cef 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -19,7 +19,7 @@ namespace osu.Game.Online internal static readonly IReadOnlyList<(Type derivedType, Type baseType)> BASE_TYPE_MAPPING = new[] { (typeof(ChangeTeamRequest), typeof(MatchUserRequest)), - (typeof(MatchStartCountdownRequest), typeof(MatchUserRequest)), + (typeof(StartMatchCountdownRequest), typeof(MatchUserRequest)), (typeof(StopCountdownRequest), typeof(MatchUserRequest)), (typeof(CountdownChangedEvent), typeof(MatchServerEvent)), (typeof(TeamVersusRoomState), typeof(MatchRoomState)), diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index f9f070f17a..381c5ea712 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Debug.Assert(clickOperation == null); clickOperation = ongoingOperationTracker.BeginOperation(); - Client.SendMatchRequest(new MatchStartCountdownRequest { Delay = duration }).ContinueWith(_ => endOperation()); + Client.SendMatchRequest(new StartMatchCountdownRequest { Delay = duration }).ContinueWith(_ => endOperation()); } private void endOperation() diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index a1ae1aa171..4066cd0c4a 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -309,7 +309,7 @@ namespace osu.Game.Tests.Visual.Multiplayer switch (request) { - case MatchStartCountdownRequest matchCountdownRequest: + case StartMatchCountdownRequest matchCountdownRequest: countdownStopSource?.Cancel(); var stopSource = countdownStopSource = new CancellationTokenSource(); From 0adad3a599904f62a9f7908ae85b678c1b83e0c8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 19 Mar 2022 04:01:35 +0300 Subject: [PATCH 028/285] Handle potential null room hosts --- .../OnlinePlay/Multiplayer/Participants/ParticipantsList.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs index 6169ad6a9e..22c9940cd4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs @@ -66,8 +66,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants if (currentHostPanel != null && panels.Contains(currentHostPanel)) panels.SetLayoutPosition(currentHostPanel, 0); + currentHostPanel = null; + // Change position of new host to display above all participants. - panels.SetLayoutPosition(currentHostPanel = panels.Single(u => u.User.Equals(Room.Host)), -1); + if (Room.Host != null) + panels.SetLayoutPosition(currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(Room.Host)), -1); } } } From 9afe82a0d522adb13b9c58dc40c95fbbd66e9403 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 19 Mar 2022 14:54:58 +0300 Subject: [PATCH 029/285] Fix potentially null drawable call to `SetLayoutPosition` --- .../Multiplayer/Participants/ParticipantsList.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs index 22c9940cd4..14b930f115 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs @@ -70,7 +70,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants // Change position of new host to display above all participants. if (Room.Host != null) - panels.SetLayoutPosition(currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(Room.Host)), -1); + { + currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(Room.Host)); + + if (currentHostPanel != null) + panels.SetLayoutPosition(currentHostPanel, -1); + } } } } From 4630aa15cc06a1efd1df0fe1486777c50d6999db Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 22 Mar 2022 12:54:10 +0900 Subject: [PATCH 030/285] Apply refactorings according to reviews --- .../Match/MultiplayerReadyButton.cs | 41 ++++++------------- .../Multiplayer/TestMultiplayerClient.cs | 8 +--- 2 files changed, 13 insertions(+), 36 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 381c5ea712..4c4cc87f6d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -282,12 +282,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready); int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating); - - string countdownText = room.Countdown == null ? string.Empty : $"Starting in {room.Countdown.EndTime - DateTimeOffset.Now:mm\\:ss}"; string countText = $"({countReady} / {countTotal} ready)"; if (room.Countdown != null) { + string countdownText = $"Starting in {room.Countdown.EndTime - DateTimeOffset.Now:mm\\:ss}"; + switch (localUser?.State) { default: @@ -329,37 +329,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match var localUser = multiplayerClient.LocalUser; - if (room.Countdown != null) + switch (localUser?.State) { - switch (localUser?.State) - { - default: - setGreen(); - break; + default: + setGreen(); + break; - case MultiplayerUserState.Spectating: - case MultiplayerUserState.Ready: + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + if (room?.Host?.Equals(localUser) == true && room.Countdown == null) + setGreen(); + else setYellow(); - break; - } - } - else - { - switch (localUser?.State) - { - default: - setGreen(); - break; - case MultiplayerUserState.Spectating: - case MultiplayerUserState.Ready: - if (room?.Host?.Equals(localUser) == true) - setGreen(); - else - setYellow(); - - break; - } + break; } void setYellow() diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 4066cd0c4a..309ca6ca58 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -322,13 +322,7 @@ namespace osu.Game.Tests.Visual.Multiplayer async Task start() { - try - { - await lastCountdownTask; - } - catch (OperationCanceledException) - { - } + await lastCountdownTask; Schedule(() => { From d0fee53e1fb26766ebd02d550243abbb625f58cd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 18 Mar 2022 22:00:39 +0900 Subject: [PATCH 031/285] Implement auto countdown timers Change to using TimeSpan --- .../Online/Multiplayer/MultiplayerClient.cs | 5 ++- .../Multiplayer/MultiplayerRoomSettings.cs | 9 ++++- osu.Game/Online/Rooms/Room.cs | 11 +++++ .../Match/MultiplayerMatchSettingsOverlay.cs | 40 ++++++++++++++++++- .../Match/MultiplayerReadyButton.cs | 4 +- .../Screens/OnlinePlay/OnlinePlayComposite.cs | 3 ++ 6 files changed, 66 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 2d5496c5c1..de17c012eb 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -239,7 +239,9 @@ namespace osu.Game.Online.Multiplayer /// The new password, if any. /// The type of the match, if any. /// The new queue mode, if any. - public Task ChangeSettings(Optional name = default, Optional password = default, Optional matchType = default, Optional queueMode = default) + /// The new auto-start countdown duration, if any. + public Task ChangeSettings(Optional name = default, Optional password = default, Optional matchType = default, Optional queueMode = default, + Optional autoStartDuration = default) { if (Room == null) throw new InvalidOperationException("Must be joined to a match to change settings."); @@ -250,6 +252,7 @@ namespace osu.Game.Online.Multiplayer Password = password.GetOr(Room.Settings.Password), MatchType = matchType.GetOr(Room.Settings.MatchType), QueueMode = queueMode.GetOr(Room.Settings.QueueMode), + AutoStartDuration = autoStartDuration.GetOr(Room.Settings.AutoStartDuration), }); } diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index c392260a22..f8b238d61b 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -28,6 +28,9 @@ namespace osu.Game.Online.Multiplayer [Key(4)] public QueueMode QueueMode { get; set; } = QueueMode.HostOnly; + [Key(5)] + public TimeSpan AutoStartDuration { get; set; } + public bool Equals(MultiplayerRoomSettings? other) { if (ReferenceEquals(this, other)) return true; @@ -37,13 +40,15 @@ namespace osu.Game.Online.Multiplayer && Name.Equals(other.Name, StringComparison.Ordinal) && PlaylistItemId == other.PlaylistItemId && MatchType == other.MatchType - && QueueMode == other.QueueMode; + && QueueMode == other.QueueMode + && AutoStartDuration == other.AutoStartDuration; } public override string ToString() => $"Name:{Name}" + $" Password:{(string.IsNullOrEmpty(Password) ? "no" : "yes")}" + $" Type:{MatchType}" + $" Item:{PlaylistItemId}" - + $" Queue:{QueueMode}"; + + $" Queue:{QueueMode}" + + $" Start:{AutoStartDuration}"; } } diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 543b176b51..76bac44242 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -92,6 +92,16 @@ namespace osu.Game.Online.Rooms set => QueueMode.Value = value; } + [Cached] + public readonly Bindable AutoStartDuration = new Bindable(); + + [JsonProperty("start_duration")] + private ushort autoStartDuration + { + get => (ushort)AutoStartDuration.Value.TotalSeconds; + set => AutoStartDuration.Value = TimeSpan.FromSeconds(value); + } + [Cached] public readonly Bindable MaxParticipants = new Bindable(); @@ -172,6 +182,7 @@ namespace osu.Game.Online.Rooms EndDate.Value = other.EndDate.Value; UserScore.Value = other.UserScore.Value; QueueMode.Value = other.QueueMode.Value; + AutoStartDuration.Value = other.AutoStartDuration.Value; DifficultyRange.Value = other.DifficultyRange.Value; PlaylistItemStats.Value = other.PlaylistItemStats.Value; CurrentPlaylistItem.Value = other.CurrentPlaylistItem.Value; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index be98a9d4e9..2918e066c1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.ComponentModel; using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -21,6 +22,7 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; +using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { @@ -64,6 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public OsuSpriteText ErrorText; + private OsuEnumDropdown startModeDropdown; private OsuSpriteText typeLabel; private LoadingLayer loadingLayer; @@ -204,6 +207,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match RelativeSizeAxes = Axes.X } } + }, + new Section("Auto start") + { + Child = new Container + { + RelativeSizeAxes = Axes.X, + Height = 40, + Child = startModeDropdown = new OsuEnumDropdown + { + RelativeSizeAxes = Axes.X + } + } } }, }, @@ -327,6 +342,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match RoomID.BindValueChanged(roomId => playlistContainer.Alpha = roomId.NewValue == null ? 1 : 0, true); Password.BindValueChanged(password => PasswordTextBox.Text = password.NewValue ?? string.Empty, true); QueueMode.BindValueChanged(mode => QueueModeDropdown.Current.Value = mode.NewValue, true); + AutoStartDuration.BindValueChanged(duration => startModeDropdown.Current.Value = (StartMode)(int)duration.NewValue.TotalSeconds, true); operationInProgress.BindTo(ongoingOperationTracker.InProgress); operationInProgress.BindValueChanged(v => @@ -363,6 +379,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Debug.Assert(applyingSettingsOperation == null); applyingSettingsOperation = ongoingOperationTracker.BeginOperation(); + TimeSpan autoStartDuration = TimeSpan.FromSeconds((int)startModeDropdown.Current.Value); + // If the client is already in a room, update via the client. // Otherwise, update the room directly in preparation for it to be submitted to the API on match creation. if (client.Room != null) @@ -371,7 +389,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match name: NameField.Text, password: PasswordTextBox.Text, matchType: TypePicker.Current.Value, - queueMode: QueueModeDropdown.Current.Value) + queueMode: QueueModeDropdown.Current.Value, + autoStartDuration: autoStartDuration) .ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) @@ -387,6 +406,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match room.Type.Value = TypePicker.Current.Value; room.Password.Value = PasswordTextBox.Current.Value; room.QueueMode.Value = QueueModeDropdown.Current.Value; + room.AutoStartDuration.Value = autoStartDuration; if (int.TryParse(MaxParticipantsField.Text, out int max)) room.MaxParticipants.Value = max; @@ -452,5 +472,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Triangles.ColourDark = colours.YellowDark; } } + + private enum StartMode + { + [Description("Off")] + Off = 0, + + [Description("30 seconds")] + Seconds_30 = 30, + + [Description("1 minute")] + Seconds_60 = 60, + + [Description("3 minutes")] + Seconds_180 = 180, + + [Description("5 minutes")] + Seconds_300 = 300 + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 381c5ea712..930ddc3d64 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -120,7 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match clickOperation = ongoingOperationTracker.BeginOperation(); // Ensure the current user becomes ready before being able to do anything else (start match, stop countdown, unready). - if (!isReady() || !Client.IsHost) + if (!isReady() || !Client.IsHost || Room.Settings.AutoStartDuration != TimeSpan.Zero) { toggleReady(); return; @@ -183,7 +183,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating); - if (Room.Countdown != null) + if (Room.Countdown != null || Room.Settings.AutoStartDuration != TimeSpan.Zero) countdownButton.Alpha = 0; else { diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 95d9b2af15..88354c8646 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -81,6 +81,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected Bindable QueueMode { get; private set; } + [Resolved(typeof(Room))] + protected Bindable AutoStartDuration { get; private set; } + [Resolved(CanBeNull = true)] private IBindable subScreenSelectedItem { get; set; } From 23be0f1be90ce5275c80fb717603ac4686285878 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 22 Mar 2022 14:17:20 +0900 Subject: [PATCH 032/285] Remove availability picker for now --- .../Match/MultiplayerMatchSettingsOverlay.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 2918e066c1..aef04c106d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -58,7 +58,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public Action SettingsApplied; public OsuTextBox NameField, MaxParticipantsField; - public RoomAvailabilityPicker AvailabilityPicker; public MatchTypePicker TypePicker; public OsuEnumDropdown QueueModeDropdown; public OsuTextBox PasswordTextBox; @@ -166,14 +165,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match LengthLimit = 100, }, }, - new Section("Room visibility") - { - Alpha = disabled_alpha, - Child = AvailabilityPicker = new RoomAvailabilityPicker - { - Enabled = { Value = false } - }, - }, new Section("Game type") { Child = new FillFlowContainer @@ -336,7 +327,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue.GetLocalisableDescription(), true); RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true); - Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true); Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true); MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); RoomID.BindValueChanged(roomId => playlistContainer.Alpha = roomId.NewValue == null ? 1 : 0, true); @@ -402,7 +392,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match else { room.Name.Value = NameField.Text; - room.Availability.Value = AvailabilityPicker.Current.Value; room.Type.Value = TypePicker.Current.Value; room.Password.Value = PasswordTextBox.Current.Value; room.QueueMode.Value = QueueModeDropdown.Current.Value; From 2c4a6c246592cb8cb142aeb8a0704d34715cd30d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Mar 2022 16:46:42 +0900 Subject: [PATCH 033/285] Add missing async safeties to new tests --- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 12 ++++++------ .../Multiplayer/TestSceneMultiplayerReadyButton.cs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index e38da96bd5..6300dfa381 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -424,7 +424,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != multiplayerClient.Room?.Playlist.First().BeatmapID); - AddStep("start match externally", () => multiplayerClient.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely()); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); @@ -462,7 +462,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != multiplayerClient.Room?.Playlist.First().RulesetID); - AddStep("start match externally", () => multiplayerClient.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely()); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); @@ -500,7 +500,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); - AddStep("start match externally", () => multiplayerClient.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely()); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); @@ -535,7 +535,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddStep("start match externally", () => multiplayerClient.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely()); AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen()); } @@ -568,7 +568,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddStep("start match externally", () => multiplayerClient.StartMatch()); + AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely()); AddStep("restore beatmap", () => { @@ -883,7 +883,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("start match by other user", () => { multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready); - multiplayerClient.StartMatch(); + multiplayerClient.StartMatch().WaitSafely(); }); AddUntilStep("wait for loading", () => multiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index fc47861576..0315e5dfe4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(2) })); + AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(2) }).WaitSafely()); ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); @@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(1) })); + AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(1) }).WaitSafely()); AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null); AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID)); From 483fb84b56bda3e3d6633c63d6daf2d74cd4e5ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Mar 2022 16:50:13 +0900 Subject: [PATCH 034/285] Fix typo in `FinishCountdown` method --- .../Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs | 6 +++--- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 0315e5dfe4..7bf03caa88 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); - AddStep("finish countdown", () => MultiplayerClient.FinishCountDown()); + AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); } @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); - AddStep("finish countdown", () => MultiplayerClient.FinishCountDown()); + AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); } @@ -158,7 +158,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddStep("finish countdown", () => MultiplayerClient.FinishCountDown()); + AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); AddUntilStep("match not started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.Open); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 309ca6ca58..2b03017905 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -300,7 +300,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private CancellationTokenSource? countdownStopSource; private Task countdownTask = Task.CompletedTask; - public void FinishCountDown() => countdownFinishSource?.Cancel(); + public void FinishCountdown() => countdownFinishSource?.Cancel(); public override async Task SendMatchRequest(MatchUserRequest request) { From 09ec49e6fa68e714730043d9ec723bc352456017 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Mar 2022 18:30:54 +0900 Subject: [PATCH 035/285] Rename realm-backed resource store in preparation for non-legacy usage --- osu.Game/Skinning/DefaultLegacySkin.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 2 +- ...sedSkinResourceStore.cs => RealmBackedResourceStore.cs} | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) rename osu.Game/Skinning/{LegacyDatabasedSkinResourceStore.cs => RealmBackedResourceStore.cs} (83%) diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index c7033d37dc..3a8464879a 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -34,7 +34,7 @@ namespace osu.Game.Skinning resources, // A default legacy skin may still have a skin.ini if it is modified by the user. // We must specify the stream directly as we are redirecting storage to the osu-resources location for other files. - new LegacyDatabasedSkinResourceStore(skin, resources.Files).GetStream("skin.ini") + new RealmBackedResourceStore(skin, resources.Files).GetStream("skin.ini") ) { Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 359d9e5624..9d71fff92f 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -51,7 +51,7 @@ namespace osu.Game.Skinning [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] public LegacySkin(SkinInfo skin, IStorageResourceProvider resources) - : this(skin, new LegacyDatabasedSkinResourceStore(skin, resources.Files), resources, "skin.ini") + : this(skin, new RealmBackedResourceStore(skin, resources.Files), resources, "skin.ini") { } diff --git a/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs similarity index 83% rename from osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs rename to osu.Game/Skinning/RealmBackedResourceStore.cs index cd90fea9bb..93ffbe4f44 100644 --- a/osu.Game/Skinning/LegacyDatabasedSkinResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -4,21 +4,22 @@ using System.Collections.Generic; using osu.Framework.Extensions; using osu.Framework.IO.Stores; +using osu.Game.Database; using osu.Game.Extensions; namespace osu.Game.Skinning { - public class LegacyDatabasedSkinResourceStore : ResourceStore + public class RealmBackedResourceStore : ResourceStore { private readonly Dictionary fileToStoragePathMapping = new Dictionary(); - public LegacyDatabasedSkinResourceStore(SkinInfo source, IResourceStore underlyingStore) + public RealmBackedResourceStore(IHasRealmFiles source, IResourceStore underlyingStore) : base(underlyingStore) { initialiseFileCache(source); } - private void initialiseFileCache(SkinInfo source) + private void initialiseFileCache(IHasRealmFiles source) { fileToStoragePathMapping.Clear(); foreach (var f in source.Files) From 9138aaf78039b39a7fcba9c2b9ada333c5346ae5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Mar 2022 10:37:53 +0900 Subject: [PATCH 036/285] Split MultiplayerReadyButton --- .../Visual/Multiplayer/QueueModeTestScene.cs | 4 +- .../TestSceneAllPlayersQueueMode.cs | 8 +- ...utton.cs => TestSceneMatchStartControl.cs} | 80 +++--- .../Multiplayer/TestSceneMultiplayer.cs | 1 + .../TestSceneMultiplayerMatchSubScreen.cs | 2 +- .../TestSceneMultiplayerSpectateButton.cs | 6 +- .../Multiplayer/Match/CountdownButton.cs | 87 +++++++ ...yerReadyButton.cs => MatchStartControl.cs} | 230 +----------------- .../Match/MultiplayerMatchFooter.cs | 2 +- .../Multiplayer/Match/ReadyButton.cs | 162 ++++++++++++ 10 files changed, 303 insertions(+), 279 deletions(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneMultiplayerReadyButton.cs => TestSceneMatchStartControl.cs} (79%) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs rename osu.Game/Screens/OnlinePlay/Multiplayer/Match/{MultiplayerReadyButton.cs => MatchStartControl.cs} (51%) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index df8f63f6ed..fd43674b3b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -95,10 +95,10 @@ namespace osu.Game.Tests.Visual.Multiplayer protected void RunGameplay() { AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded); AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs index 266ac60168..582dacb332 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs @@ -102,10 +102,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); AddAssert("ruleset is correct", () => ((Player)CurrentScreen).Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); @@ -119,10 +119,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); AddAssert("mods are correct", () => !((Player)CurrentScreen).Mods.Value.Any()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs similarity index 79% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 7bf03caa88..4e54740a69 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -27,9 +27,9 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMultiplayerReadyButton : MultiplayerTestScene + public class TestSceneMatchStartControl : MultiplayerTestScene { - private MultiplayerReadyButton button; + private MatchStartControl control; private BeatmapSetInfo importedSet; private readonly Bindable selectedItem = new Bindable(); @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - Child = button = new MultiplayerReadyButton + Child = control = new MatchStartControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -74,17 +74,17 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestStartWithCountdown() { - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); - AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); + AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); } @@ -92,17 +92,17 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestCancelCountdown() { - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); @@ -119,10 +119,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(2) }).WaitSafely()); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); } @@ -132,25 +132,25 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddAssert("countdown button is visible", () => this.ChildrenOfType().Single().IsPresent); - AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + AddAssert("countdown button is visible", () => this.ChildrenOfType().Single().IsPresent); + AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); - AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); - AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value); } [Test] public void TestSpectatingDuringCountdownWithNoReadyUsersCancelsCountdown() { - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -168,12 +168,12 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddAssert("ready button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + AddAssert("ready button enabled", () => this.ChildrenOfType().Single().Enabled.Value); } [Test] @@ -199,7 +199,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID)); AddUntilStep("local user is host", () => MultiplayerClient.Room?.Host?.Equals(MultiplayerClient.LocalUser) == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null); } @@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("ensure ready button enabled", () => { - readyButton = button.ChildrenOfType().Single(); + readyButton = control.ChildrenOfType().Single(); return readyButton.Enabled.Value; }); @@ -230,10 +230,10 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); } @@ -249,7 +249,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); verifyGameplayStartFlow(); @@ -264,7 +264,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0)); verifyGameplayStartFlow(); @@ -279,14 +279,14 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0)); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); - AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); + AddAssert("ready button enabled", () => control.ChildrenOfType().Single().Enabled.Value); } [TestCase(true)] @@ -304,7 +304,7 @@ namespace osu.Game.Tests.Visual.Multiplayer if (!isHost) AddStep("transfer host", () => MultiplayerClient.TransferHost(2)); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddRepeatStep("change user ready state", () => { @@ -322,7 +322,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void verifyGameplayStartFlow() { AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); AddStep("finish gameplay", () => @@ -331,7 +331,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay); }); - AddUntilStep("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); + AddUntilStep("ready button enabled", () => control.ChildrenOfType().Single().Enabled.Value); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 6300dfa381..d0765fc4b3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -41,6 +41,7 @@ using osu.Game.Screens.Ranking; using osu.Game.Screens.Spectate; using osu.Game.Tests.Resources; using osuTK.Input; +using ReadyButton = osu.Game.Screens.OnlinePlay.Components.ReadyButton; namespace osu.Game.Tests.Visual.Multiplayer { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index a51d4678fe..850a115f4c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for spectating user state", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("match started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 13c9e021db..07ac580276 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene { private MultiplayerSpectateButton spectateButton; - private MultiplayerReadyButton readyButton; + private MatchStartControl startControl; private readonly Bindable selectedItem = new Bindable(); @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Origin = Anchor.Centre, Size = new Vector2(200, 50), }, - readyButton = new MultiplayerReadyButton + startControl = new MatchStartControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -146,6 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer => AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); private void assertReadyButtonEnablement(bool shouldBeEnabled) - => AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); + => AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => startControl.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs new file mode 100644 index 0000000000..e598f6670c --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs @@ -0,0 +1,87 @@ +// 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 Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public class CountdownButton : IconButton, IHasPopover + { + private static readonly TimeSpan[] available_delays = + { + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(30), + TimeSpan.FromMinutes(1), + TimeSpan.FromMinutes(2) + }; + + public new Action Action; + + private readonly Drawable background; + + public CountdownButton() + { + Icon = FontAwesome.Solid.CaretDown; + IconScale = new Vector2(0.6f); + + Add(background = new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }); + + base.Action = this.ShowPopover; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = colours.Green; + } + + public Popover GetPopover() + { + var flow = new FillFlowContainer + { + Width = 200, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + }; + + foreach (var duration in available_delays) + { + flow.Add(new PopoverButton + { + RelativeSizeAxes = Axes.X, + Text = $"Start match in {duration.Humanize()}", + BackgroundColour = background.Colour, + Action = () => + { + Action(duration); + this.HidePopover(); + } + }); + } + + return new OsuPopover { Child = flow }; + } + + public class PopoverButton : OsuButton + { + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs similarity index 51% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index 4c4cc87f6d..d97ac601d5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -4,32 +4,21 @@ using System; using System.Diagnostics; using System.Linq; -using Humanizer; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Localisation; using osu.Framework.Threading; -using osu.Game.Graphics; -using osu.Game.Graphics.Backgrounds; -using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class MultiplayerReadyButton : MultiplayerRoomComposite + public class MatchStartControl : MultiplayerRoomComposite { [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } @@ -47,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private ScheduledDelegate readySampleDelegate; private IBindable operationInProgress; - public MultiplayerReadyButton() + public MatchStartControl() { InternalChild = new GridContainer { @@ -231,220 +220,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match countReady = newCountReady; }); } - - public class ReadyButton : Components.ReadyButton - { - public new Triangles Triangles => base.Triangles; - - [Resolved] - private MultiplayerClient multiplayerClient { get; set; } - - [Resolved] - private OsuColour colours { get; set; } - - [CanBeNull] - private MultiplayerRoom room => multiplayerClient.Room; - - protected override void LoadComplete() - { - base.LoadComplete(); - - multiplayerClient.RoomUpdated += () => Scheduler.AddOnce(onRoomUpdated); - onRoomUpdated(); - } - - protected override void Update() - { - base.Update(); - - if (room?.Countdown != null) - { - // Update the countdown timer. - onRoomUpdated(); - } - } - - private void onRoomUpdated() - { - updateButtonText(); - updateButtonColour(); - } - - private void updateButtonText() - { - if (room == null) - { - Text = "Ready"; - return; - } - - var localUser = multiplayerClient.LocalUser; - - int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready); - int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating); - string countText = $"({countReady} / {countTotal} ready)"; - - if (room.Countdown != null) - { - string countdownText = $"Starting in {room.Countdown.EndTime - DateTimeOffset.Now:mm\\:ss}"; - - switch (localUser?.State) - { - default: - Text = $"Ready ({countdownText.ToLowerInvariant()})"; - break; - - case MultiplayerUserState.Spectating: - case MultiplayerUserState.Ready: - Text = $"{countdownText} {countText}"; - break; - } - } - else - { - switch (localUser?.State) - { - default: - Text = "Ready"; - break; - - case MultiplayerUserState.Spectating: - case MultiplayerUserState.Ready: - Text = room.Host?.Equals(localUser) == true - ? $"Start match {countText}" - : $"Waiting for host... {countText}"; - - break; - } - } - } - - private void updateButtonColour() - { - if (room == null) - { - setGreen(); - return; - } - - var localUser = multiplayerClient.LocalUser; - - switch (localUser?.State) - { - default: - setGreen(); - break; - - case MultiplayerUserState.Spectating: - case MultiplayerUserState.Ready: - if (room?.Host?.Equals(localUser) == true && room.Countdown == null) - setGreen(); - else - setYellow(); - - break; - } - - void setYellow() - { - BackgroundColour = colours.YellowDark; - Triangles.ColourDark = colours.YellowDark; - Triangles.ColourLight = colours.Yellow; - } - - void setGreen() - { - BackgroundColour = colours.Green; - Triangles.ColourDark = colours.Green; - Triangles.ColourLight = colours.GreenLight; - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (multiplayerClient != null) - multiplayerClient.RoomUpdated -= onRoomUpdated; - } - - public override LocalisableString TooltipText - { - get - { - if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready) - return "Cancel countdown"; - - return base.TooltipText; - } - } - } - - public class CountdownButton : IconButton, IHasPopover - { - private static readonly TimeSpan[] available_delays = - { - TimeSpan.FromSeconds(10), - TimeSpan.FromSeconds(30), - TimeSpan.FromMinutes(1), - TimeSpan.FromMinutes(2) - }; - - public new Action Action; - - private readonly Drawable background; - - public CountdownButton() - { - Icon = FontAwesome.Solid.CaretDown; - IconScale = new Vector2(0.6f); - - Add(background = new Box - { - RelativeSizeAxes = Axes.Both, - Depth = float.MaxValue - }); - - base.Action = this.ShowPopover; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - background.Colour = colours.Green; - } - - public Popover GetPopover() - { - var flow = new FillFlowContainer - { - Width = 200, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(2), - }; - - foreach (var duration in available_delays) - { - flow.Add(new PopoverButton - { - RelativeSizeAxes = Axes.X, - Text = $"Start match in {duration.Humanize()}", - BackgroundColour = background.Colour, - Action = () => - { - Action(duration); - this.HidePopover(); - } - }); - } - - return new OsuPopover { Child = flow }; - } - - public class PopoverButton : OsuButton - { - } - } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index b4fce5903b..a07c95bca8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match RelativeSizeAxes = Axes.Both, }, null, - new MultiplayerReadyButton + new MatchStartControl { RelativeSizeAxes = Axes.Both, }, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs new file mode 100644 index 0000000000..8e3a9f9349 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs @@ -0,0 +1,162 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public class ReadyButton : Components.ReadyButton + { + public new Triangles Triangles => base.Triangles; + + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + + [CanBeNull] + private MultiplayerRoom room => multiplayerClient.Room; + + protected override void LoadComplete() + { + base.LoadComplete(); + + multiplayerClient.RoomUpdated += () => Scheduler.AddOnce(onRoomUpdated); + onRoomUpdated(); + } + + protected override void Update() + { + base.Update(); + + if (room?.Countdown != null) + { + // Update the countdown timer. + onRoomUpdated(); + } + } + + private void onRoomUpdated() + { + updateButtonText(); + updateButtonColour(); + } + + private void updateButtonText() + { + if (room == null) + { + Text = "Ready"; + return; + } + + var localUser = multiplayerClient.LocalUser; + + int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready); + int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating); + string countText = $"({countReady} / {countTotal} ready)"; + + if (room.Countdown != null) + { + string countdownText = $"Starting in {room.Countdown.EndTime - DateTimeOffset.Now:mm\\:ss}"; + + switch (localUser?.State) + { + default: + Text = $"Ready ({countdownText.ToLowerInvariant()})"; + break; + + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + Text = $"{countdownText} {countText}"; + break; + } + } + else + { + switch (localUser?.State) + { + default: + Text = "Ready"; + break; + + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + Text = room.Host?.Equals(localUser) == true + ? $"Start match {countText}" + : $"Waiting for host... {countText}"; + + break; + } + } + } + + private void updateButtonColour() + { + if (room == null) + { + setGreen(); + return; + } + + var localUser = multiplayerClient.LocalUser; + + switch (localUser?.State) + { + default: + setGreen(); + break; + + case MultiplayerUserState.Spectating: + case MultiplayerUserState.Ready: + if (room?.Host?.Equals(localUser) == true && room.Countdown == null) + setGreen(); + else + setYellow(); + + break; + } + + void setYellow() + { + BackgroundColour = colours.YellowDark; + Triangles.ColourDark = colours.YellowDark; + Triangles.ColourLight = colours.Yellow; + } + + void setGreen() + { + BackgroundColour = colours.Green; + Triangles.ColourDark = colours.Green; + Triangles.ColourLight = colours.GreenLight; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (multiplayerClient != null) + multiplayerClient.RoomUpdated -= onRoomUpdated; + } + + public override LocalisableString TooltipText + { + get + { + if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready) + return "Cancel countdown"; + + return base.TooltipText; + } + } + } +} From 6b712be97d08de140bd6d3b0cb81d8d90412194b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Mar 2022 10:40:05 +0900 Subject: [PATCH 037/285] Remove PopoverButton class --- .../Visual/Multiplayer/TestSceneMatchStartControl.cs | 8 ++++---- .../OnlinePlay/Multiplayer/Match/CountdownButton.cs | 6 +----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 4e54740a69..b98676e737 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -150,7 +150,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs index e598f6670c..e37168bf25 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match foreach (var duration in available_delays) { - flow.Add(new PopoverButton + flow.Add(new OsuButton { RelativeSizeAxes = Axes.X, Text = $"Start match in {duration.Humanize()}", @@ -79,9 +79,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match return new OsuPopover { Child = flow }; } - - public class PopoverButton : OsuButton - { - } } } From d4ad4ac9db8c76be1cd9545944dca12e582d41f9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Mar 2022 10:50:05 +0900 Subject: [PATCH 038/285] Limit countdown updates to once per second --- .../Multiplayer/Match/ReadyButton.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs index 8e3a9f9349..b37d990466 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs @@ -6,6 +6,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Localisation; +using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Online.Multiplayer; @@ -33,21 +34,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match onRoomUpdated(); } - protected override void Update() - { - base.Update(); - - if (room?.Countdown != null) - { - // Update the countdown timer. - onRoomUpdated(); - } - } + private ScheduledDelegate countdownUpdateDelegate; private void onRoomUpdated() { updateButtonText(); updateButtonColour(); + + if (room?.Countdown != null) + countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 1000, true); + else + { + countdownUpdateDelegate?.Cancel(); + countdownUpdateDelegate = null; + } } private void updateButtonText() From 5c4a74378dec7f2b64f5de49abde8779c6793486 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Mar 2022 18:36:42 +0900 Subject: [PATCH 039/285] Remove `Textures` and `Samples` initialisation to `Skin` --- osu.Game/Skinning/LegacySkin.cs | 25 --------- osu.Game/Skinning/Skin.cs | 99 ++++++++++++++++++--------------- 2 files changed, 53 insertions(+), 71 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 9d71fff92f..5533615568 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -27,12 +27,6 @@ namespace osu.Game.Skinning { public class LegacySkin : Skin { - [CanBeNull] - protected TextureStore Textures; - - [CanBeNull] - protected ISampleStore Samples; - /// /// Whether texture for the keys exists. /// Used to determine if the mania ruleset is skinned. @@ -77,18 +71,6 @@ namespace osu.Game.Skinning protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore storage, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] Stream configurationStream) : base(skin, resources, configurationStream) { - if (storage != null) - { - var samples = resources?.AudioManager?.GetSampleStore(storage); - if (samples != null) - samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; - - Samples = samples; - Textures = new TextureStore(resources?.CreateTextureLoaderStore(storage)); - - (storage as ResourceStore)?.AddExtension("ogg"); - } - // todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution. hasKeyTexture = new Lazy(() => this.GetAnimation( lookupForMania(new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value ?? "mania-key1", true, @@ -551,12 +533,5 @@ namespace osu.Game.Skinning // Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle"). yield return componentName.Split('/').Last(); } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - Textures?.Dispose(); - Samples?.Dispose(); - } } } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 931bdfed48..d1ea7aa300 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -16,7 +16,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Database; -using osu.Game.Extensions; using osu.Game.IO; using osu.Game.Screens.Play.HUD; @@ -24,8 +23,19 @@ namespace osu.Game.Skinning { public abstract class Skin : IDisposable, ISkin { + /// + /// A texture store which can be used to perform user file loops for this skin. + /// + [CanBeNull] + protected TextureStore Textures { get; set; } + + /// + /// A sample store which can be used to perform user file loops for this skin. + /// + [CanBeNull] + protected ISampleStore Samples { get; set; } + public readonly Live SkinInfo; - private readonly IStorageResourceProvider resources; public SkinConfiguration Configuration { get; set; } @@ -41,16 +51,30 @@ namespace osu.Game.Skinning public abstract IBindable GetConfig(TLookup lookup); + private readonly RealmBackedResourceStore skinStorage; + protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null) { - SkinInfo = resources?.RealmAccess != null - ? skin.ToLive(resources.RealmAccess) - // This path should only be used in some tests. - : skin.ToLiveUnmanaged(); + if (resources.RealmAccess != null) + { + SkinInfo = skin.ToLive(resources.RealmAccess); + skinStorage = new RealmBackedResourceStore(skin, resources.Files); - this.resources = resources; + var samples = resources?.AudioManager?.GetSampleStore(skinStorage); + if (samples != null) + samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; - configurationStream ??= getConfigurationStream(); + Samples = samples; + Textures = new TextureStore(resources?.CreateTextureLoaderStore(skinStorage)); + + skinStorage.AddExtension("ogg"); + } + else + { + SkinInfo = skin.ToLiveUnmanaged(); + } + + configurationStream ??= skinStorage?.GetStream(@"skin.ini"); if (configurationStream != null) // stream will be closed after use by LineBufferedReader. @@ -59,40 +83,30 @@ namespace osu.Game.Skinning Configuration = new SkinConfiguration(); // skininfo files may be null for default skin. - SkinInfo.PerformRead(s => + foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget))) { - // we may want to move this to some kind of async operation in the future. - foreach (SkinnableTarget skinnableTarget in Enum.GetValues(typeof(SkinnableTarget))) + string filename = $"{skinnableTarget}.json"; + + byte[] bytes = skinStorage?.Get(filename); + + if (bytes == null) + continue; + + try { - string filename = $"{skinnableTarget}.json"; + string jsonContent = Encoding.UTF8.GetString(bytes); + var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); - // skininfo files may be null for default skin. - var fileInfo = s.Files.FirstOrDefault(f => f.Filename == filename); - - if (fileInfo == null) + if (deserializedContent == null) continue; - byte[] bytes = resources?.Files.Get(fileInfo.File.GetStoragePath()); - - if (bytes == null) - continue; - - try - { - string jsonContent = Encoding.UTF8.GetString(bytes); - var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); - - if (deserializedContent == null) - continue; - - DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray(); - } - catch (Exception ex) - { - Logger.Error(ex, "Failed to load skin configuration."); - } + DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray(); } - }); + catch (Exception ex) + { + Logger.Error(ex, "Failed to load skin configuration."); + } + } } protected virtual void ParseConfigurationStream(Stream stream) @@ -101,16 +115,6 @@ namespace osu.Game.Skinning Configuration = new LegacySkinDecoder().Decode(reader); } - private Stream getConfigurationStream() - { - string path = SkinInfo.PerformRead(s => s.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath()); - - if (string.IsNullOrEmpty(path)) - return null; - - return resources?.Files.GetStream(path); - } - /// /// Remove all stored customisations for the provided target. /// @@ -168,6 +172,9 @@ namespace osu.Game.Skinning return; isDisposed = true; + + Textures?.Dispose(); + Samples?.Dispose(); } #endregion From b4d89b4e313863e6dec8ce85a4948ef4d9321b63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Mar 2022 18:42:45 +0900 Subject: [PATCH 040/285] Replace duplicate `LegacySkinResourceStore` class with `RealmBackedResourceStore` --- osu.Game/Skinning/LegacyBeatmapSkin.cs | 2 +- osu.Game/Skinning/LegacySkinResourceStore.cs | 39 -------------------- 2 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 osu.Game/Skinning/LegacySkinResourceStore.cs diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index f80a980351..2ddf79ed5a 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -21,7 +21,7 @@ namespace osu.Game.Skinning protected override bool UseCustomSampleBanks => true; public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IResourceStore storage, IStorageResourceProvider resources) - : base(createSkinInfo(beatmapInfo), new LegacySkinResourceStore(beatmapInfo.BeatmapSet, storage), resources, beatmapInfo.Path) + : base(createSkinInfo(beatmapInfo), new RealmBackedResourceStore(beatmapInfo.BeatmapSet, storage), resources, beatmapInfo.Path) { // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) Configuration.AllowDefaultComboColoursFallback = false; diff --git a/osu.Game/Skinning/LegacySkinResourceStore.cs b/osu.Game/Skinning/LegacySkinResourceStore.cs deleted file mode 100644 index 2487a469c8..0000000000 --- a/osu.Game/Skinning/LegacySkinResourceStore.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Extensions; -using osu.Framework.IO.Stores; -using osu.Game.Database; -using osu.Game.Extensions; - -namespace osu.Game.Skinning -{ - public class LegacySkinResourceStore : ResourceStore - { - private readonly IHasNamedFiles source; - - public LegacySkinResourceStore(IHasNamedFiles source, IResourceStore underlyingStore) - : base(underlyingStore) - { - this.source = source; - } - - protected override IEnumerable GetFilenames(string name) - { - foreach (string filename in base.GetFilenames(name)) - { - string path = getPathForFile(filename.ToStandardisedPath()); - if (path != null) - yield return path; - } - } - - private string getPathForFile(string filename) => - source.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath(); - - public override IEnumerable GetAvailableResources() => source.Files.Select(f => f.Filename); - } -} From 35d2f973a39b533ed1cb7392d31d1d8299e9b0c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Mar 2022 19:07:05 +0900 Subject: [PATCH 041/285] Prefer provided resource store over realm backed to keep tests working --- osu.Game/Skinning/LegacySkin.cs | 2 +- osu.Game/Skinning/Skin.cs | 39 +++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 5533615568..84844c4502 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -69,7 +69,7 @@ namespace osu.Game.Skinning /// Access to raw game resources. /// An optional stream containing the contents of a skin.ini file. protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore storage, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] Stream configurationStream) - : base(skin, resources, configurationStream) + : base(skin, resources, storage, configurationStream) { // todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution. hasKeyTexture = new Lazy(() => this.GetAnimation( diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index d1ea7aa300..a47da70cd6 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -13,6 +13,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Database; @@ -51,30 +52,36 @@ namespace osu.Game.Skinning public abstract IBindable GetConfig(TLookup lookup); - private readonly RealmBackedResourceStore skinStorage; - - protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null) + protected Skin(SkinInfo skin, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage = null, [CanBeNull] Stream configurationStream = null) { - if (resources.RealmAccess != null) + if (resources != null) { - SkinInfo = skin.ToLive(resources.RealmAccess); - skinStorage = new RealmBackedResourceStore(skin, resources.Files); + if (resources.RealmAccess != null) + { + SkinInfo = skin.ToLive(resources.RealmAccess); - var samples = resources?.AudioManager?.GetSampleStore(skinStorage); + if (storage == null) + { + var realmStorage = new RealmBackedResourceStore(skin, resources.Files); + realmStorage.AddExtension("ogg"); + + storage = realmStorage; + } + } + else + { + SkinInfo = skin.ToLiveUnmanaged(); + } + + var samples = resources.AudioManager?.GetSampleStore(storage); if (samples != null) samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; Samples = samples; - Textures = new TextureStore(resources?.CreateTextureLoaderStore(skinStorage)); - - skinStorage.AddExtension("ogg"); - } - else - { - SkinInfo = skin.ToLiveUnmanaged(); + Textures = new TextureStore(resources.CreateTextureLoaderStore(storage)); } - configurationStream ??= skinStorage?.GetStream(@"skin.ini"); + configurationStream ??= storage?.GetStream(@"skin.ini"); if (configurationStream != null) // stream will be closed after use by LineBufferedReader. @@ -87,7 +94,7 @@ namespace osu.Game.Skinning { string filename = $"{skinnableTarget}.json"; - byte[] bytes = skinStorage?.Get(filename); + byte[] bytes = storage?.Get(filename); if (bytes == null) continue; From a7f63fb034d5174dc7e51e46fdfce88a06a4a89c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Mar 2022 19:23:22 +0900 Subject: [PATCH 042/285] Make providing a custom `ResourceStore` to `LegacyBeatmapSkin` optional (for tests only) --- osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs | 2 +- osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs | 2 +- .../Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 +- osu.Game/Skinning/LegacyBeatmapSkin.cs | 5 +++-- osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs | 2 +- osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs | 2 +- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs index 71544e94f3..0c1981b35d 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Skins public class BeatmapSkinSource : LegacyBeatmapSkin { public BeatmapSkinSource() - : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null) + : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null) { } diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs index 870d6d8f57..d3cacaa88c 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs @@ -202,7 +202,7 @@ namespace osu.Game.Tests.Skins public class BeatmapSkinSource : LegacyBeatmapSkin { public BeatmapSkinSource() - : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null) + : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null) { } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index eb1695b3df..53364b6d89 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestEmptyLegacyBeatmapSkinFallsBack() { - CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null)); + CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null)); AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value)); } diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index d3f356bb24..7d28208157 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -225,7 +225,7 @@ namespace osu.Game.Beatmaps { try { - return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources); + return new LegacyBeatmapSkin(BeatmapInfo, resources); } catch (Exception e) { diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 2ddf79ed5a..9b3548912c 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using JetBrains.Annotations; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -20,8 +21,8 @@ namespace osu.Game.Skinning protected override bool AllowManiaSkin => false; protected override bool UseCustomSampleBanks => true; - public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IResourceStore storage, IStorageResourceProvider resources) - : base(createSkinInfo(beatmapInfo), new RealmBackedResourceStore(beatmapInfo.BeatmapSet, storage), resources, beatmapInfo.Path) + public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IStorageResourceProvider resources, [CanBeNull] IResourceStore storage = null) + : base(createSkinInfo(beatmapInfo), storage ?? new RealmBackedResourceStore(beatmapInfo.BeatmapSet, resources.Files), resources, beatmapInfo.Path) { // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) Configuration.AllowDefaultComboColoursFallback = false; diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 2a3e51b4f5..c13504c3de 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -206,7 +206,7 @@ namespace osu.Game.Tests.Beatmaps this.resources = resources; } - protected internal override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, resources); + protected internal override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resources, resourceStore); } } } diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs index 5c522058d9..fee650ded0 100644 --- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -112,7 +112,7 @@ namespace osu.Game.Tests.Beatmaps public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod; public TestBeatmapSkin(BeatmapInfo beatmapInfo, bool hasColours) - : base(beatmapInfo, new ResourceStore(), null) + : base(beatmapInfo, null, new ResourceStore()) { if (hasColours) { From e1236e07add705b02c145518169152e7c4e2fcd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 13:36:33 +0900 Subject: [PATCH 043/285] Fix extensions not being specified in time for realm file caching --- osu.Game/Skinning/LegacyBeatmapSkin.cs | 5 +++-- osu.Game/Skinning/RealmBackedResourceStore.cs | 9 +++++++- osu.Game/Skinning/Skin.cs | 21 ++++++++----------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 9b3548912c..d2428b0ccf 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -21,8 +21,9 @@ namespace osu.Game.Skinning protected override bool AllowManiaSkin => false; protected override bool UseCustomSampleBanks => true; - public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IStorageResourceProvider resources, [CanBeNull] IResourceStore storage = null) - : base(createSkinInfo(beatmapInfo), storage ?? new RealmBackedResourceStore(beatmapInfo.BeatmapSet, resources.Files), resources, beatmapInfo.Path) + public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage = null) + : base(createSkinInfo(beatmapInfo), storage ?? (resources != null ? new RealmBackedResourceStore(beatmapInfo.BeatmapSet, resources.Files, new[] { @"ogg" }) : null), resources, + beatmapInfo.Path) { // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) Configuration.AllowDefaultComboColoursFallback = false; diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index 93ffbe4f44..fc9036727f 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -13,9 +13,16 @@ namespace osu.Game.Skinning { private readonly Dictionary fileToStoragePathMapping = new Dictionary(); - public RealmBackedResourceStore(IHasRealmFiles source, IResourceStore underlyingStore) + public RealmBackedResourceStore(IHasRealmFiles source, IResourceStore underlyingStore, string[] extensions = null) : base(underlyingStore) { + // Must be initialised before the file cache. + if (extensions != null) + { + foreach (string extension in extensions) + AddExtension(extension); + } + initialiseFileCache(source); } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index a47da70cd6..654c4e75f7 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -60,25 +60,22 @@ namespace osu.Game.Skinning { SkinInfo = skin.ToLive(resources.RealmAccess); - if (storage == null) - { - var realmStorage = new RealmBackedResourceStore(skin, resources.Files); - realmStorage.AddExtension("ogg"); - - storage = realmStorage; - } + storage ??= new RealmBackedResourceStore(skin, resources.Files, new[] { @"ogg" }); } else { SkinInfo = skin.ToLiveUnmanaged(); } - var samples = resources.AudioManager?.GetSampleStore(storage); - if (samples != null) - samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; + if (storage != null) + { + var samples = resources.AudioManager?.GetSampleStore(storage); + if (samples != null) + samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; - Samples = samples; - Textures = new TextureStore(resources.CreateTextureLoaderStore(storage)); + Samples = samples; + Textures = new TextureStore(resources.CreateTextureLoaderStore(storage)); + } } configurationStream ??= storage?.GetStream(@"skin.ini"); From 32e2cfb8ee710feabc140f1920c6b4e4c7150062 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 13:38:32 +0900 Subject: [PATCH 044/285] Leave realm resource store construction to base class --- osu.Game/Skinning/LegacySkin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 84844c4502..238cda1151 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -45,7 +45,7 @@ namespace osu.Game.Skinning [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] public LegacySkin(SkinInfo skin, IStorageResourceProvider resources) - : this(skin, new RealmBackedResourceStore(skin, resources.Files), resources, "skin.ini") + : this(skin, null, resources, "skin.ini") { } From 3c38b142281e2bb90023be88b96ce0ac38a8b745 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 13:14:56 +0900 Subject: [PATCH 045/285] Documentation improvements --- osu.Game/Skinning/LegacyBeatmapSkin.cs | 23 +++++++++++++++++++++-- osu.Game/Skinning/LegacySkin.cs | 5 +++-- osu.Game/Skinning/Skin.cs | 7 +++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index d2428b0ccf..7b8b659473 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -9,6 +9,7 @@ using osu.Framework.IO.Stores; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; +using osu.Game.Database; using osu.Game.IO; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; @@ -21,14 +22,28 @@ namespace osu.Game.Skinning protected override bool AllowManiaSkin => false; protected override bool UseCustomSampleBanks => true; + /// + /// Construct a new legacy beatmap skin instance. + /// + /// The model for this beatmap. + /// Access to raw game resources. + /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage = null) - : base(createSkinInfo(beatmapInfo), storage ?? (resources != null ? new RealmBackedResourceStore(beatmapInfo.BeatmapSet, resources.Files, new[] { @"ogg" }) : null), resources, + : base(createSkinInfo(beatmapInfo), storage ?? createRealmBackedStore(beatmapInfo, resources), resources, beatmapInfo.Path) { // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) Configuration.AllowDefaultComboColoursFallback = false; } + private static IResourceStore createRealmBackedStore(BeatmapInfo beatmapInfo, [CanBeNull] IStorageResourceProvider resources) + { + if (resources == null) + return null; + + return new RealmBackedResourceStore(beatmapInfo.BeatmapSet, resources.Files, new[] { @"ogg" }); + } + public override Drawable GetDrawableComponent(ISkinComponent component) { if (component is SkinnableTargetComponent targetComponent) @@ -79,6 +94,10 @@ namespace osu.Game.Skinning } private static SkinInfo createSkinInfo(BeatmapInfo beatmapInfo) => - new SkinInfo { Name = beatmapInfo.ToString(), Creator = beatmapInfo.Metadata.Author.Username ?? string.Empty }; + new SkinInfo + { + Name = beatmapInfo.ToString(), + Creator = beatmapInfo.Metadata.Author.Username ?? string.Empty + }; } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 238cda1151..1559b9af53 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; +using osu.Game.Database; using osu.Game.IO; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -53,7 +54,7 @@ namespace osu.Game.Skinning /// Construct a new legacy skin instance. /// /// The model for this skin. - /// A storage for looking up files within this skin using user-facing filenames. + /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. /// Access to raw game resources. /// The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file. protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore storage, [CanBeNull] IStorageResourceProvider resources, string configurationFilename) @@ -65,7 +66,7 @@ namespace osu.Game.Skinning /// Construct a new legacy skin instance. /// /// The model for this skin. - /// A storage for looking up files within this skin using user-facing filenames. + /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. /// Access to raw game resources. /// An optional stream containing the contents of a skin.ini file. protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore storage, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] Stream configurationStream) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 654c4e75f7..84041deb1a 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -52,6 +52,13 @@ namespace osu.Game.Skinning public abstract IBindable GetConfig(TLookup lookup); + /// + /// Construct a new skin. + /// + /// The skin's metadata. Usually a live realm object. + /// Access to game-wide resources. + /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. + /// An optional stream which will be used to read the skin configuration. If null, the configuration will be retrieved from the storage using "skin.ini". protected Skin(SkinInfo skin, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage = null, [CanBeNull] Stream configurationStream = null) { if (resources != null) From 00aea9bef02f559210bef8ef898a07191c03f96e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 13:46:19 +0900 Subject: [PATCH 046/285] Only use legacy resources lookup for protected (aka default) skin --- osu.Game/Skinning/DefaultLegacySkin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 3a8464879a..7d7d2db7f3 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -30,7 +30,7 @@ namespace osu.Game.Skinning public DefaultLegacySkin(SkinInfo skin, IStorageResourceProvider resources) : base( skin, - new NamespacedResourceStore(resources.Resources, "Skins/Legacy"), + skin.Protected ? new NamespacedResourceStore(resources.Resources, "Skins/Legacy") : null, resources, // A default legacy skin may still have a skin.ini if it is modified by the user. // We must specify the stream directly as we are redirecting storage to the osu-resources location for other files. From a5acd38fd5b68a9e137a1852aa3683b94a60bd12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 14:14:55 +0900 Subject: [PATCH 047/285] Fix `HitObjectSampleTest` adding null files to realm models --- osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index c13504c3de..630fbda688 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -96,12 +96,14 @@ namespace osu.Game.Tests.Beatmaps AddStep("setup skins", () => { userSkinInfo.Files.Clear(); - userSkinInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = userFile }, userFile)); + if (!string.IsNullOrEmpty(userFile)) + userSkinInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = userFile }, userFile)); Debug.Assert(beatmapInfo.BeatmapSet != null); beatmapInfo.BeatmapSet.Files.Clear(); - beatmapInfo.BeatmapSet.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = beatmapFile }, beatmapFile)); + if (!string.IsNullOrEmpty(beatmapFile)) + beatmapInfo.BeatmapSet.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = beatmapFile }, beatmapFile)); // Need to refresh the cached skin source to refresh the skin resource store. dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, this)); From e56d13d8be0ac9774aecf477cb43c24987295dd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 14:21:35 +0900 Subject: [PATCH 048/285] Fix realm backed store not being initialised for some tests --- osu.Game/Skinning/Skin.cs | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 84041deb1a..4c2b58bbc2 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -63,26 +63,18 @@ namespace osu.Game.Skinning { if (resources != null) { - if (resources.RealmAccess != null) - { - SkinInfo = skin.ToLive(resources.RealmAccess); + SkinInfo = resources.RealmAccess != null + ? skin.ToLive(resources.RealmAccess) + : skin.ToLiveUnmanaged(); - storage ??= new RealmBackedResourceStore(skin, resources.Files, new[] { @"ogg" }); - } - else - { - SkinInfo = skin.ToLiveUnmanaged(); - } + storage ??= new RealmBackedResourceStore(skin, resources.Files, new[] { @"ogg" }); - if (storage != null) - { - var samples = resources.AudioManager?.GetSampleStore(storage); - if (samples != null) - samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; + var samples = resources.AudioManager?.GetSampleStore(storage); + if (samples != null) + samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; - Samples = samples; - Textures = new TextureStore(resources.CreateTextureLoaderStore(storage)); - } + Samples = samples; + Textures = new TextureStore(resources.CreateTextureLoaderStore(storage)); } configurationStream ??= storage?.GetStream(@"skin.ini"); From b48aa1d8fa934eaedb0c03f65e630d3217c94826 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 14:23:07 +0900 Subject: [PATCH 049/285] Ensure `HitObjectSampleTest`'s `TestWorkingBeatmap` provides the marking resource store correctly --- .../Tests/Beatmaps/HitObjectSampleTest.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 630fbda688..4667a385b3 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -193,22 +193,32 @@ namespace osu.Game.Tests.Beatmaps } } - private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap + private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap, IStorageResourceProvider { private readonly BeatmapInfo skinBeatmapInfo; - private readonly IResourceStore resourceStore; private readonly IStorageResourceProvider resources; - public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, IStorageResourceProvider resources) + public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore accessMarkingResourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, + IStorageResourceProvider resources) : base(beatmap, storyboard, referenceClock, resources.AudioManager) { this.skinBeatmapInfo = skinBeatmapInfo; - this.resourceStore = resourceStore; + Files = accessMarkingResourceStore; this.resources = resources; } - protected internal override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resources, resourceStore); + protected internal override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, this); + + public AudioManager AudioManager => resources.AudioManager; + + public IResourceStore Files { get; } + + public IResourceStore Resources => resources.Resources; + + public RealmAccess RealmAccess => resources.RealmAccess; + + public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => resources.CreateTextureLoaderStore(underlyingStore); } } } From 6c405f1dee7242d5fc7dbdff300658d4693e3221 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 14:31:24 +0900 Subject: [PATCH 050/285] Remove `storage` override from `LegacyBeatmapSkin` --- osu.Game/Skinning/LegacyBeatmapSkin.cs | 10 ++++------ osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 7b8b659473..6dc68b23d3 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -9,7 +9,6 @@ using osu.Framework.IO.Stores; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; -using osu.Game.Database; using osu.Game.IO; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; @@ -27,10 +26,8 @@ namespace osu.Game.Skinning /// /// The model for this beatmap. /// Access to raw game resources. - /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. - public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage = null) - : base(createSkinInfo(beatmapInfo), storage ?? createRealmBackedStore(beatmapInfo, resources), resources, - beatmapInfo.Path) + public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, [CanBeNull] IStorageResourceProvider resources) + : base(createSkinInfo(beatmapInfo), createRealmBackedStore(beatmapInfo, resources), resources, beatmapInfo.Path) { // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) Configuration.AllowDefaultComboColoursFallback = false; @@ -39,7 +36,8 @@ namespace osu.Game.Skinning private static IResourceStore createRealmBackedStore(BeatmapInfo beatmapInfo, [CanBeNull] IStorageResourceProvider resources) { if (resources == null) - return null; + // should only ever be used in tests. + return new ResourceStore(); return new RealmBackedResourceStore(beatmapInfo.BeatmapSet, resources.Files, new[] { @"ogg" }); } diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs index fee650ded0..b2b348d869 100644 --- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -112,7 +112,7 @@ namespace osu.Game.Tests.Beatmaps public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod; public TestBeatmapSkin(BeatmapInfo beatmapInfo, bool hasColours) - : base(beatmapInfo, null, new ResourceStore()) + : base(beatmapInfo, null) { if (hasColours) { From 9d3c6ade6237c347bef8ad060ea755f2f2cf9d15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 14:38:40 +0900 Subject: [PATCH 051/285] Remove unnecessary skin reading hack in `DefaultLegacySkin` --- osu.Game/Skinning/DefaultLegacySkin.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 7d7d2db7f3..e7d0ed793f 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -30,11 +30,10 @@ namespace osu.Game.Skinning public DefaultLegacySkin(SkinInfo skin, IStorageResourceProvider resources) : base( skin, + // In the case of the actual default legacy skin (ie. the fallback one, which a user hasn't applied any modifications to) we want to use the game provided resources. skin.Protected ? new NamespacedResourceStore(resources.Resources, "Skins/Legacy") : null, resources, - // A default legacy skin may still have a skin.ini if it is modified by the user. - // We must specify the stream directly as we are redirecting storage to the osu-resources location for other files. - new RealmBackedResourceStore(skin, resources.Files).GetStream("skin.ini") + "skin.ini" ) { Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); From d0ea1739b4767427c6ccd1b8de425a5fa4059be3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 14:53:00 +0900 Subject: [PATCH 052/285] Remove skin configuration stream logic as it is no longer required --- osu.Game/Skinning/LegacySkin.cs | 14 +------------- osu.Game/Skinning/Skin.cs | 6 +++--- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 1559b9af53..6752851ca6 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -58,19 +58,7 @@ namespace osu.Game.Skinning /// Access to raw game resources. /// The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file. protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore storage, [CanBeNull] IStorageResourceProvider resources, string configurationFilename) - : this(skin, storage, resources, string.IsNullOrEmpty(configurationFilename) ? null : storage?.GetStream(configurationFilename)) - { - } - - /// - /// Construct a new legacy skin instance. - /// - /// The model for this skin. - /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. - /// Access to raw game resources. - /// An optional stream containing the contents of a skin.ini file. - protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore storage, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] Stream configurationStream) - : base(skin, resources, storage, configurationStream) + : base(skin, resources, storage, configurationFilename) { // todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution. hasKeyTexture = new Lazy(() => this.GetAnimation( diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 4c2b58bbc2..72bbca4903 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -58,8 +58,8 @@ namespace osu.Game.Skinning /// The skin's metadata. Usually a live realm object. /// Access to game-wide resources. /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. - /// An optional stream which will be used to read the skin configuration. If null, the configuration will be retrieved from the storage using "skin.ini". - protected Skin(SkinInfo skin, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage = null, [CanBeNull] Stream configurationStream = null) + /// An optional filename to read the skin configuration from. If not provided, the configuration will be retrieved from the storage using "skin.ini". + protected Skin(SkinInfo skin, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage = null, [CanBeNull] string configurationFilename = "skin.ini") { if (resources != null) { @@ -77,7 +77,7 @@ namespace osu.Game.Skinning Textures = new TextureStore(resources.CreateTextureLoaderStore(storage)); } - configurationStream ??= storage?.GetStream(@"skin.ini"); + var configurationStream = storage?.GetStream(configurationFilename); if (configurationStream != null) // stream will be closed after use by LineBufferedReader. From 7a1909bf970acd9f5a2ebcce6387a644b0ec3002 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 15:02:01 +0900 Subject: [PATCH 053/285] Change parameter order of `LegacySkin` to put `IStorageResourceProvider` first The optional resource store should not be before the (basically) required resource provider. --- .../CatchSkinColourDecodingTest.cs | 2 +- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 2 +- osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 2 +- osu.Game/Skinning/DefaultLegacySkin.cs | 2 +- osu.Game/Skinning/LegacyBeatmapSkin.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 6 +++--- osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs | 2 +- osu.Game/Tests/Visual/SkinnableTestScene.cs | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs index e70def7f8b..ea3c0f3f4a 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Tests { public TestLegacySkin(SkinInfo skin, IResourceStore storage) // Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null). - : base(skin, storage, null, "skin.ini") + : base(skin, null, storage, "skin.ini") { } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index d19b3c71f1..0d436c1ef7 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -175,7 +175,7 @@ namespace osu.Game.Tests.Beatmaps.Formats private class TestLegacySkin : LegacySkin { public TestLegacySkin(IResourceStore storage, string fileName) - : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName) + : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, null, storage, fileName) { } } diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 6457a23a1b..cfb158cc5a 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -148,7 +148,7 @@ namespace osu.Game.Tests.Gameplay private class TestSkin : LegacySkin { public TestSkin(string resourceName, IStorageResourceProvider resources) - : base(DefaultLegacySkin.CreateInfo(), new TestResourceStore(resourceName), resources, "skin.ini") + : base(DefaultLegacySkin.CreateInfo(), resources, new TestResourceStore(resourceName), "skin.ini") { } } diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index e7d0ed793f..be1b7b8f25 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -30,9 +30,9 @@ namespace osu.Game.Skinning public DefaultLegacySkin(SkinInfo skin, IStorageResourceProvider resources) : base( skin, + resources, // In the case of the actual default legacy skin (ie. the fallback one, which a user hasn't applied any modifications to) we want to use the game provided resources. skin.Protected ? new NamespacedResourceStore(resources.Resources, "Skins/Legacy") : null, - resources, "skin.ini" ) { diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 6dc68b23d3..e3685c986b 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -27,7 +27,7 @@ namespace osu.Game.Skinning /// The model for this beatmap. /// Access to raw game resources. public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, [CanBeNull] IStorageResourceProvider resources) - : base(createSkinInfo(beatmapInfo), createRealmBackedStore(beatmapInfo, resources), resources, beatmapInfo.Path) + : base(createSkinInfo(beatmapInfo), resources, createRealmBackedStore(beatmapInfo, resources), beatmapInfo.Path) { // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) Configuration.AllowDefaultComboColoursFallback = false; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 6752851ca6..fa581f98e0 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -46,7 +46,7 @@ namespace osu.Game.Skinning [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] public LegacySkin(SkinInfo skin, IStorageResourceProvider resources) - : this(skin, null, resources, "skin.ini") + : this(skin, resources, null, "skin.ini") { } @@ -54,10 +54,10 @@ namespace osu.Game.Skinning /// Construct a new legacy skin instance. /// /// The model for this skin. - /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. /// Access to raw game resources. + /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. /// The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file. - protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore storage, [CanBeNull] IStorageResourceProvider resources, string configurationFilename) + protected LegacySkin(SkinInfo skin, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage, string configurationFilename) : base(skin, resources, storage, configurationFilename) { // todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution. diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs index b2b348d869..531c38b655 100644 --- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Beatmaps public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.LightCyan; public TestSkin(bool hasCustomColours) - : base(new SkinInfo(), new ResourceStore(), null, string.Empty) + : base(new SkinInfo(), null, new ResourceStore(), string.Empty) { if (hasCustomColours) { diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 1107089a46..c8fc988ffd 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual private readonly bool extrapolateAnimations; public TestLegacySkin(SkinInfo skin, IResourceStore storage, IStorageResourceProvider resources, bool extrapolateAnimations) - : base(skin, storage, resources, "skin.ini") + : base(skin, resources, storage, "skin.ini") { this.extrapolateAnimations = extrapolateAnimations; } From 078288a616319eed8e5a9a3d653a22d92ae3c88a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 15:05:01 +0900 Subject: [PATCH 054/285] Make "skin.ini" the default skin filename and remove redundant parameters --- osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs | 2 +- osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 2 +- osu.Game/Skinning/DefaultLegacySkin.cs | 3 +-- osu.Game/Skinning/LegacySkin.cs | 4 ++-- osu.Game/Skinning/Skin.cs | 2 +- osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs | 3 +-- osu.Game/Tests/Visual/SkinnableTestScene.cs | 2 +- 7 files changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs index ea3c0f3f4a..bb3a724b91 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Tests { public TestLegacySkin(SkinInfo skin, IResourceStore storage) // Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null). - : base(skin, null, storage, "skin.ini") + : base(skin, null, storage) { } } diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index cfb158cc5a..76ec35d87d 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -148,7 +148,7 @@ namespace osu.Game.Tests.Gameplay private class TestSkin : LegacySkin { public TestSkin(string resourceName, IStorageResourceProvider resources) - : base(DefaultLegacySkin.CreateInfo(), resources, new TestResourceStore(resourceName), "skin.ini") + : base(DefaultLegacySkin.CreateInfo(), resources, new TestResourceStore(resourceName)) { } } diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index be1b7b8f25..f7b415e886 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -32,8 +32,7 @@ namespace osu.Game.Skinning skin, resources, // In the case of the actual default legacy skin (ie. the fallback one, which a user hasn't applied any modifications to) we want to use the game provided resources. - skin.Protected ? new NamespacedResourceStore(resources.Resources, "Skins/Legacy") : null, - "skin.ini" + skin.Protected ? new NamespacedResourceStore(resources.Resources, "Skins/Legacy") : null ) { Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index fa581f98e0..1c2ca797c6 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -46,7 +46,7 @@ namespace osu.Game.Skinning [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] public LegacySkin(SkinInfo skin, IStorageResourceProvider resources) - : this(skin, resources, null, "skin.ini") + : this(skin, resources, null) { } @@ -57,7 +57,7 @@ namespace osu.Game.Skinning /// Access to raw game resources. /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. /// The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file. - protected LegacySkin(SkinInfo skin, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage, string configurationFilename) + protected LegacySkin(SkinInfo skin, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage, string configurationFilename = @"skin.ini") : base(skin, resources, storage, configurationFilename) { // todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution. diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 72bbca4903..e28c35937d 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -59,7 +59,7 @@ namespace osu.Game.Skinning /// Access to game-wide resources. /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. /// An optional filename to read the skin configuration from. If not provided, the configuration will be retrieved from the storage using "skin.ini". - protected Skin(SkinInfo skin, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage = null, [CanBeNull] string configurationFilename = "skin.ini") + protected Skin(SkinInfo skin, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage = null, [CanBeNull] string configurationFilename = @"skin.ini") { if (resources != null) { diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs index 531c38b655..597c5e9a2b 100644 --- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -7,7 +7,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; -using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Skinning; @@ -141,7 +140,7 @@ namespace osu.Game.Tests.Beatmaps public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.LightCyan; public TestSkin(bool hasCustomColours) - : base(new SkinInfo(), null, new ResourceStore(), string.Empty) + : base(new SkinInfo(), null, null) { if (hasCustomColours) { diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index c8fc988ffd..b4da91a97a 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual private readonly bool extrapolateAnimations; public TestLegacySkin(SkinInfo skin, IResourceStore storage, IStorageResourceProvider resources, bool extrapolateAnimations) - : base(skin, resources, storage, "skin.ini") + : base(skin, resources, storage) { this.extrapolateAnimations = extrapolateAnimations; } From f7c004720648ae8e296f3c1d2b2f49612a29f285 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Mar 2022 15:19:43 +0900 Subject: [PATCH 055/285] Send time remaining in countdowns instead --- .../Multiplayer/MultiplayerCountdown.cs | 8 +++++-- .../Multiplayer/Match/ReadyButton.cs | 23 +++++++++++++++---- .../Multiplayer/TestMultiplayerClient.cs | 2 +- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs index 63bb47b295..81190e64c9 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs @@ -5,6 +5,7 @@ using System; using MessagePack; +using osu.Game.Online.Multiplayer.Countdown; namespace osu.Game.Online.Multiplayer { @@ -16,9 +17,12 @@ namespace osu.Game.Online.Multiplayer public abstract class MultiplayerCountdown { /// - /// The time at which the countdown will end. + /// The amount of time remaining in the countdown. /// + /// + /// This is only sent once from the server upon initial retrieval of the or via a . + /// [Key(0)] - public DateTimeOffset EndTime { get; set; } + public TimeSpan TimeRemaining { get; set; } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs index b37d990466..007e055d8c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs @@ -34,12 +34,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match onRoomUpdated(); } + private MultiplayerCountdown countdown; + private DateTimeOffset countdownReceivedTime; private ScheduledDelegate countdownUpdateDelegate; private void onRoomUpdated() { - updateButtonText(); - updateButtonColour(); + if (countdown == null && room?.Countdown != null) + countdownReceivedTime = DateTimeOffset.Now; + + countdown = room?.Countdown; if (room?.Countdown != null) countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 1000, true); @@ -48,6 +52,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match countdownUpdateDelegate?.Cancel(); countdownUpdateDelegate = null; } + + updateButtonText(); + updateButtonColour(); } private void updateButtonText() @@ -64,9 +71,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating); string countText = $"({countReady} / {countTotal} ready)"; - if (room.Countdown != null) + if (countdown != null) { - string countdownText = $"Starting in {room.Countdown.EndTime - DateTimeOffset.Now:mm\\:ss}"; + TimeSpan timeElapsed = DateTimeOffset.Now - countdownReceivedTime; + TimeSpan countdownRemaining; + + if (timeElapsed > countdown.TimeRemaining) + countdownRemaining = TimeSpan.Zero; + else + countdownRemaining = countdown.TimeRemaining - timeElapsed; + + string countdownText = $"Starting in {countdownRemaining:mm\\:ss}"; switch (localUser?.State) { diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 2b03017905..938147e05e 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -315,7 +315,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var stopSource = countdownStopSource = new CancellationTokenSource(); var finishSource = countdownFinishSource = new CancellationTokenSource(); var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, finishSource.Token); - var countdown = new MatchStartCountdown { EndTime = DateTimeOffset.Now + matchCountdownRequest.Delay }; + var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Delay }; Task lastCountdownTask = countdownTask; countdownTask = start(); From a83a90e675f39a1646b8091c6da4c7eaeefe1d9d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 23 Mar 2022 15:21:16 +0900 Subject: [PATCH 056/285] Rename countdown Delay -> Duration --- .../Visual/Multiplayer/TestSceneMatchStartControl.cs | 4 ++-- .../Multiplayer/Countdown/StartMatchCountdownRequest.cs | 2 +- .../Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs | 2 +- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index b98676e737..7ef9505d8c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(2) }).WaitSafely()); + AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(2) }).WaitSafely()); ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); @@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(1) }).WaitSafely()); + AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely()); AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null); AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID)); diff --git a/osu.Game/Online/Multiplayer/Countdown/StartMatchCountdownRequest.cs b/osu.Game/Online/Multiplayer/Countdown/StartMatchCountdownRequest.cs index 9e6967af9d..08eab26090 100644 --- a/osu.Game/Online/Multiplayer/Countdown/StartMatchCountdownRequest.cs +++ b/osu.Game/Online/Multiplayer/Countdown/StartMatchCountdownRequest.cs @@ -18,6 +18,6 @@ namespace osu.Game.Online.Multiplayer.Countdown /// How long the countdown should last. /// [Key(0)] - public TimeSpan Delay { get; set; } + public TimeSpan Duration { get; set; } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index d97ac601d5..9470fb1d68 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -150,7 +150,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Debug.Assert(clickOperation == null); clickOperation = ongoingOperationTracker.BeginOperation(); - Client.SendMatchRequest(new StartMatchCountdownRequest { Delay = duration }).ContinueWith(_ => endOperation()); + Client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation()); } private void endOperation() diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 938147e05e..1f20292437 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -315,7 +315,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var stopSource = countdownStopSource = new CancellationTokenSource(); var finishSource = countdownFinishSource = new CancellationTokenSource(); var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, finishSource.Token); - var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Delay }; + var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration }; Task lastCountdownTask = countdownTask; countdownTask = start(); @@ -335,7 +335,7 @@ namespace osu.Game.Tests.Visual.Multiplayer try { - await Task.Delay(matchCountdownRequest.Delay, cancellationSource.Token).ConfigureAwait(false); + await Task.Delay(matchCountdownRequest.Duration, cancellationSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) { From 05c7e09d79fa1f667c8bdfe7472eaa3a7677fa2f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 23:57:14 +0900 Subject: [PATCH 057/285] Make `Textures` and `Samples` read-only --- .../Skinning/LegacySkinTextureFallbackTest.cs | 54 +++++++++++++++---- osu.Game/Skinning/Skin.cs | 4 +- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs index 69e66942ab..7516e7500b 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs @@ -1,12 +1,21 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; -using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Audio; using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Game.Database; +using osu.Game.IO; using osu.Game.Skinning; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace osu.Game.Tests.NonVisual.Skinning { @@ -71,7 +80,7 @@ namespace osu.Game.Tests.NonVisual.Skinning var texture = legacySkin.GetTexture(requestedComponent); Assert.IsNotNull(texture); - Assert.AreEqual(textureStore.Textures[expectedTexture], texture); + Assert.AreEqual(textureStore.Textures[expectedTexture].Width, texture.Width); Assert.AreEqual(expectedScale, texture.ScaleAdjust); } @@ -88,23 +97,50 @@ namespace osu.Game.Tests.NonVisual.Skinning private class TestLegacySkin : LegacySkin { - public TestLegacySkin(TextureStore textureStore) - : base(new SkinInfo(), null, null, string.Empty) + public TestLegacySkin(IResourceStore textureStore) + : base(new SkinInfo(), new TestResourceProvider(textureStore), null, string.Empty) { - Textures = textureStore; + } + + private class TestResourceProvider : IStorageResourceProvider + { + private readonly IResourceStore textureStore; + + public TestResourceProvider(IResourceStore textureStore) + { + this.textureStore = textureStore; + } + + public AudioManager AudioManager => null; + public IResourceStore Files => null; + public IResourceStore Resources => null; + public RealmAccess RealmAccess => null; + public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => textureStore; } } - private class TestTextureStore : TextureStore + private class TestTextureStore : IResourceStore { - public readonly Dictionary Textures; + public readonly Dictionary Textures; public TestTextureStore(params string[] fileNames) { - Textures = fileNames.ToDictionary(fileName => fileName, fileName => new Texture(1, 1)); + // use an incrementing width to allow assertion matching on correct textures as they turn from uploads into actual textures. + int width = 1; + Textures = fileNames.ToDictionary(fileName => fileName, fileName => new TextureUpload(new Image(width, width++))); } - public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => Textures.GetValueOrDefault(name); + public TextureUpload Get(string name) => Textures.GetValueOrDefault(name); + + public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => Task.FromResult(Get(name)); + + public Stream GetStream(string name) => throw new NotImplementedException(); + + public IEnumerable GetAvailableResources() => throw new NotImplementedException(); + + public void Dispose() + { + } } } } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index e28c35937d..b6f46a0d81 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -28,13 +28,13 @@ namespace osu.Game.Skinning /// A texture store which can be used to perform user file loops for this skin. /// [CanBeNull] - protected TextureStore Textures { get; set; } + protected TextureStore Textures { get; } /// /// A sample store which can be used to perform user file loops for this skin. /// [CanBeNull] - protected ISampleStore Samples { get; set; } + protected ISampleStore Samples { get; } public readonly Live SkinInfo; From 3e020073c50d325f9c19ded1515d0653e19f5fb6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 00:08:01 +0900 Subject: [PATCH 058/285] Convert `Skin` to use `nullable` --- osu.Game/Skinning/ISkin.cs | 18 +++++++----------- osu.Game/Skinning/Skin.cs | 36 +++++++++++++++++++++--------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs index 73f7cf6d39..4b14dcfd62 100644 --- a/osu.Game/Skinning/ISkin.cs +++ b/osu.Game/Skinning/ISkin.cs @@ -1,7 +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 JetBrains.Annotations; +#nullable enable + using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -21,16 +22,14 @@ namespace osu.Game.Skinning /// /// The requested component. /// A drawable representation for the requested component, or null if unavailable. - [CanBeNull] - Drawable GetDrawableComponent(ISkinComponent component); + Drawable? GetDrawableComponent(ISkinComponent component); /// /// Retrieve a . /// /// The requested texture. /// A matching texture, or null if unavailable. - [CanBeNull] - Texture GetTexture(string componentName) => GetTexture(componentName, default, default); + Texture? GetTexture(string componentName) => GetTexture(componentName, default, default); /// /// Retrieve a . @@ -39,23 +38,20 @@ namespace osu.Game.Skinning /// The texture wrap mode in horizontal direction. /// The texture wrap mode in vertical direction. /// A matching texture, or null if unavailable. - [CanBeNull] - Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT); + Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT); /// /// Retrieve a . /// /// The requested sample. /// A matching sample channel, or null if unavailable. - [CanBeNull] - ISample GetSample(ISampleInfo sampleInfo); + ISample? GetSample(ISampleInfo sampleInfo); /// /// Retrieve a configuration value. /// /// The requested configuration value. /// A matching value boxed in an , or null if unavailable. - [CanBeNull] - IBindable GetConfig(TLookup lookup); + IBindable? GetConfig(TLookup lookup); } } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index b6f46a0d81..643e0c38d4 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -1,12 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; -using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; @@ -27,14 +29,12 @@ namespace osu.Game.Skinning /// /// A texture store which can be used to perform user file loops for this skin. /// - [CanBeNull] - protected TextureStore Textures { get; } + protected TextureStore? Textures { get; } /// /// A sample store which can be used to perform user file loops for this skin. /// - [CanBeNull] - protected ISampleStore Samples { get; } + protected ISampleStore? Samples { get; } public readonly Live SkinInfo; @@ -44,13 +44,13 @@ namespace osu.Game.Skinning private readonly Dictionary drawableComponentInfo = new Dictionary(); - public abstract ISample GetSample(ISampleInfo sampleInfo); + public abstract ISample? GetSample(ISampleInfo sampleInfo); - public Texture GetTexture(string componentName) => GetTexture(componentName, default, default); + public Texture? GetTexture(string componentName) => GetTexture(componentName, default, default); - public abstract Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT); + public abstract Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT); - public abstract IBindable GetConfig(TLookup lookup); + public abstract IBindable? GetConfig(TLookup lookup); /// /// Construct a new skin. @@ -59,13 +59,11 @@ namespace osu.Game.Skinning /// Access to game-wide resources. /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. /// An optional filename to read the skin configuration from. If not provided, the configuration will be retrieved from the storage using "skin.ini". - protected Skin(SkinInfo skin, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage = null, [CanBeNull] string configurationFilename = @"skin.ini") + protected Skin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? storage = null, string configurationFilename = @"skin.ini") { if (resources != null) { - SkinInfo = resources.RealmAccess != null - ? skin.ToLive(resources.RealmAccess) - : skin.ToLiveUnmanaged(); + SkinInfo = skin.ToLive(resources.RealmAccess); storage ??= new RealmBackedResourceStore(skin, resources.Files, new[] { @"ogg" }); @@ -76,12 +74,20 @@ namespace osu.Game.Skinning Samples = samples; Textures = new TextureStore(resources.CreateTextureLoaderStore(storage)); } + else + { + // Generally only used for tests. + SkinInfo = skin.ToLiveUnmanaged(); + } var configurationStream = storage?.GetStream(configurationFilename); if (configurationStream != null) + { // stream will be closed after use by LineBufferedReader. ParseConfigurationStream(configurationStream); + Debug.Assert(Configuration != null); + } else Configuration = new SkinConfiguration(); @@ -90,7 +96,7 @@ namespace osu.Game.Skinning { string filename = $"{skinnableTarget}.json"; - byte[] bytes = storage?.Get(filename); + byte[]? bytes = storage?.Get(filename); if (bytes == null) continue; @@ -136,7 +142,7 @@ namespace osu.Game.Skinning DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSkinnableInfo().ToArray(); } - public virtual Drawable GetDrawableComponent(ISkinComponent component) + public virtual Drawable? GetDrawableComponent(ISkinComponent component) { switch (component) { From 194bf4fb057e60eff9781c2d85896e3b79075202 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 00:21:19 +0900 Subject: [PATCH 059/285] Convert `LegacySkin` to use `nullable` --- osu.Game/Skinning/LegacySkin.cs | 35 ++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 1c2ca797c6..20cac52d21 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; using System.Diagnostics; @@ -57,7 +59,7 @@ namespace osu.Game.Skinning /// Access to raw game resources. /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. /// The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file. - protected LegacySkin(SkinInfo skin, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage, string configurationFilename = @"skin.ini") + protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? storage, string configurationFilename = @"skin.ini") : base(skin, resources, storage, configurationFilename) { // todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution. @@ -81,7 +83,7 @@ namespace osu.Game.Skinning } } - public override IBindable GetConfig(TLookup lookup) + public override IBindable? GetConfig(TLookup lookup) { switch (lookup) { @@ -127,7 +129,7 @@ namespace osu.Game.Skinning return null; } - private IBindable lookupForMania(LegacyManiaSkinConfigurationLookup maniaLookup) + private IBindable? lookupForMania(LegacyManiaSkinConfigurationLookup maniaLookup) { if (!maniaConfigurations.TryGetValue(maniaLookup.Keys, out var existing)) maniaConfigurations[maniaLookup.Keys] = existing = new LegacyManiaSkinConfiguration(maniaLookup.Keys); @@ -267,20 +269,19 @@ namespace osu.Game.Skinning /// The source to retrieve the combo colours from. /// The preferred index for retrieving the combo colour with. /// Information on the combo whose using the returned colour. - protected virtual IBindable GetComboColour(IHasComboColours source, int colourIndex, IHasComboInformation combo) + protected virtual IBindable? GetComboColour(IHasComboColours source, int colourIndex, IHasComboInformation combo) { var colour = source.ComboColours?[colourIndex % source.ComboColours.Count]; return colour.HasValue ? new Bindable(colour.Value) : null; } - private IBindable getCustomColour(IHasCustomColours source, string lookup) + private IBindable? getCustomColour(IHasCustomColours source, string lookup) => source.CustomColours.TryGetValue(lookup, out var col) ? new Bindable(col) : null; - private IBindable getManiaImage(LegacyManiaSkinConfiguration source, string lookup) + private IBindable? getManiaImage(LegacyManiaSkinConfiguration source, string lookup) => source.ImageLookups.TryGetValue(lookup, out string image) ? new Bindable(image) : null; - [CanBeNull] - private IBindable legacySettingLookup(SkinConfiguration.LegacySetting legacySetting) + private IBindable? legacySettingLookup(SkinConfiguration.LegacySetting legacySetting) { switch (legacySetting) { @@ -292,9 +293,11 @@ namespace osu.Game.Skinning } } - [CanBeNull] - private IBindable genericLookup(TLookup lookup) + private IBindable? genericLookup(TLookup lookup) { + if (lookup == null) + return null; + try { if (Configuration.ConfigDictionary.TryGetValue(lookup.ToString(), out string val)) @@ -316,7 +319,7 @@ namespace osu.Game.Skinning return null; } - public override Drawable GetDrawableComponent(ISkinComponent component) + public override Drawable? GetDrawableComponent(ISkinComponent component) { if (base.GetDrawableComponent(component) is Drawable c) return c; @@ -385,7 +388,7 @@ namespace osu.Game.Skinning case GameplaySkinComponent resultComponent: // TODO: this should be inside the judgement pieces. - Func createDrawable = () => getJudgementAnimation(resultComponent.Component); + Func createDrawable = () => getJudgementAnimation(resultComponent.Component); // kind of wasteful that we throw this away, but should do for now. if (createDrawable() != null) @@ -404,7 +407,7 @@ namespace osu.Game.Skinning return this.GetAnimation(component.LookupName, false, false); } - private Texture getParticleTexture(HitResult result) + private Texture? getParticleTexture(HitResult result) { switch (result) { @@ -421,7 +424,7 @@ namespace osu.Game.Skinning return null; } - private Drawable getJudgementAnimation(HitResult result) + private Drawable? getJudgementAnimation(HitResult result) { switch (result) { @@ -441,7 +444,7 @@ namespace osu.Game.Skinning return null; } - public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) + public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { foreach (string name in getFallbackNames(componentName)) { @@ -469,7 +472,7 @@ namespace osu.Game.Skinning return null; } - public override ISample GetSample(ISampleInfo sampleInfo) + public override ISample? GetSample(ISampleInfo sampleInfo) { IEnumerable lookupNames; From 7296bad294c93d7e088f52a103a0f395dae1581b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 00:24:06 +0900 Subject: [PATCH 060/285] Convert `LegacyBeatmapSkin` to use `nullable` --- osu.Game/Skinning/LegacyBeatmapSkin.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index e3685c986b..16a05f4197 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -1,9 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using JetBrains.Annotations; +#nullable enable + using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.IO.Stores; using osu.Game.Audio; @@ -26,14 +28,14 @@ namespace osu.Game.Skinning /// /// The model for this beatmap. /// Access to raw game resources. - public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, [CanBeNull] IStorageResourceProvider resources) - : base(createSkinInfo(beatmapInfo), resources, createRealmBackedStore(beatmapInfo, resources), beatmapInfo.Path) + public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IStorageResourceProvider? resources) + : base(createSkinInfo(beatmapInfo), resources, createRealmBackedStore(beatmapInfo, resources), beatmapInfo.Path.AsNonNull()) { // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) Configuration.AllowDefaultComboColoursFallback = false; } - private static IResourceStore createRealmBackedStore(BeatmapInfo beatmapInfo, [CanBeNull] IStorageResourceProvider resources) + private static IResourceStore createRealmBackedStore(BeatmapInfo beatmapInfo, IStorageResourceProvider? resources) { if (resources == null) // should only ever be used in tests. @@ -42,7 +44,7 @@ namespace osu.Game.Skinning return new RealmBackedResourceStore(beatmapInfo.BeatmapSet, resources.Files, new[] { @"ogg" }); } - public override Drawable GetDrawableComponent(ISkinComponent component) + public override Drawable? GetDrawableComponent(ISkinComponent component) { if (component is SkinnableTargetComponent targetComponent) { @@ -61,7 +63,7 @@ namespace osu.Game.Skinning return base.GetDrawableComponent(component); } - public override IBindable GetConfig(TLookup lookup) + public override IBindable? GetConfig(TLookup lookup) { switch (lookup) { @@ -77,10 +79,10 @@ namespace osu.Game.Skinning return base.GetConfig(lookup); } - protected override IBindable GetComboColour(IHasComboColours source, int comboIndex, IHasComboInformation combo) + protected override IBindable? GetComboColour(IHasComboColours source, int comboIndex, IHasComboInformation combo) => base.GetComboColour(source, combo.ComboIndexWithOffsets, combo); - public override ISample GetSample(ISampleInfo sampleInfo) + public override ISample? GetSample(ISampleInfo sampleInfo) { if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0) { From 547418e47e2296e71fb6567441afb183af2152d0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 11:15:51 +0900 Subject: [PATCH 061/285] Revert "Remove PopoverButton class" This reverts commit 6b712be97d08de140bd6d3b0cb81d8d90412194b. --- .../Visual/Multiplayer/TestSceneMatchStartControl.cs | 8 ++++---- .../OnlinePlay/Multiplayer/Match/CountdownButton.cs | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index cd1eee49ec..b09fd306b7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -150,7 +150,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs index e37168bf25..e598f6670c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match foreach (var duration in available_delays) { - flow.Add(new OsuButton + flow.Add(new PopoverButton { RelativeSizeAxes = Axes.X, Text = $"Start match in {duration.Humanize()}", @@ -79,5 +79,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match return new OsuPopover { Child = flow }; } + + public class PopoverButton : OsuButton + { + } } } From a4d17a915f96c505e518dd261f9b4d1d5d967b7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 12:36:16 +0900 Subject: [PATCH 062/285] Fix incorrect HUD component fallback Legacy skins should now always show the legacy hud components. The conditional here is no longer valid as fallback lookups happen at a *skin*-fallback level rather than internal *source*-fallback. Put another way, `LegacyDefaultSkin` (with user customisations) should still display the classic HUD components even if a font is not provided, as that font will be available via the skin lookup hierarchy. The TODO removed in this commit has been already resolved so this code is no longer required. --- osu.Game/Skinning/LegacySkin.cs | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 1c2ca797c6..244774fd4c 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -356,26 +356,15 @@ namespace osu.Game.Skinning } }) { - Children = this.HasFont(LegacyFont.Score) - ? new Drawable[] - { - new LegacyComboCounter(), - new LegacyScoreCounter(), - new LegacyAccuracyCounter(), - new LegacyHealthDisplay(), - new SongProgress(), - new BarHitErrorMeter(), - } - : new Drawable[] - { - // TODO: these should fallback to using osu!classic skin textures, rather than doing this. - new DefaultComboCounter(), - new DefaultScoreCounter(), - new DefaultAccuracyCounter(), - new DefaultHealthDisplay(), - new SongProgress(), - new BarHitErrorMeter(), - } + Children = new Drawable[] + { + new LegacyComboCounter(), + new LegacyScoreCounter(), + new LegacyAccuracyCounter(), + new LegacyHealthDisplay(), + new SongProgress(), + new BarHitErrorMeter(), + } }; return skinnableTargetWrapper; From 0cd29a73b929b007c02619d96fa6b71f0834aa24 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 12:39:47 +0900 Subject: [PATCH 063/285] Fix typo in xmldocs --- osu.Game/Skinning/Skin.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index b6f46a0d81..e1fcf196f3 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -25,13 +25,13 @@ namespace osu.Game.Skinning public abstract class Skin : IDisposable, ISkin { /// - /// A texture store which can be used to perform user file loops for this skin. + /// A texture store which can be used to perform user file lookups for this skin. /// [CanBeNull] protected TextureStore Textures { get; } /// - /// A sample store which can be used to perform user file loops for this skin. + /// A sample store which can be used to perform user file lookups for this skin. /// [CanBeNull] protected ISampleStore Samples { get; } From e243a7c55d349788f85c34a1b573b88547ace35b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 12:45:11 +0900 Subject: [PATCH 064/285] Reword `storage` param xmldoc to use stronger and better defined langauge --- osu.Game/Skinning/Skin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index e1fcf196f3..e00dd950a7 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -57,7 +57,7 @@ namespace osu.Game.Skinning /// /// The skin's metadata. Usually a live realm object. /// Access to game-wide resources. - /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. + /// An optional store which will *replace* all file lookups that are usually sourced from . /// An optional filename to read the skin configuration from. If not provided, the configuration will be retrieved from the storage using "skin.ini". protected Skin(SkinInfo skin, [CanBeNull] IStorageResourceProvider resources, [CanBeNull] IResourceStore storage = null, [CanBeNull] string configurationFilename = @"skin.ini") { From 4d0b4c2541823d083c235c6b390a8d17d2f73ac9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 12:53:50 +0900 Subject: [PATCH 065/285] Fix realm potentially not being refreshed in time for test asserts in `BeatmapImporterTests` As seen at https://github.com/ppy/osu/runs/5659368512?check_suite_focus=true Went through every usage of `.Import` and added either an `EnsureLoaded`, or where that provides too many checks, an explicit `realm.Refresh()`. --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 9abd78039a..f9c13a8169 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -147,7 +147,10 @@ namespace osu.Game.Tests.Database Live? imported; using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) + { imported = await importer.Import(reader); + EnsureLoaded(realm.Realm); + } Assert.AreEqual(1, realm.Realm.All().Count()); @@ -510,6 +513,8 @@ namespace osu.Game.Tests.Database new ImportTask(zipStream, string.Empty) ); + realm.Run(r => r.Refresh()); + checkBeatmapSetCount(realm.Realm, 0); checkBeatmapCount(realm.Realm, 0); @@ -565,6 +570,8 @@ namespace osu.Game.Tests.Database { } + EnsureLoaded(realm.Realm); + checkBeatmapSetCount(realm.Realm, 1); checkBeatmapCount(realm.Realm, 12); @@ -726,6 +733,8 @@ namespace osu.Game.Tests.Database var imported = importer.Import(toImport); + realm.Run(r => r.Refresh()); + Assert.NotNull(imported); Debug.Assert(imported != null); @@ -891,6 +900,8 @@ namespace osu.Game.Tests.Database string? temp = TestResources.GetTestBeatmapForImport(); await importer.Import(temp); + EnsureLoaded(realm.Realm); + // Update via the beatmap, not the beatmap info, to ensure correct linking BeatmapSetInfo setToUpdate = realm.Realm.All().First(); From 90c7945bca3c413824618dc3ae430d072e2d7a85 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 14:26:31 +0900 Subject: [PATCH 066/285] Re-remove PopoverButton class with better test fix --- .../Visual/Multiplayer/TestSceneMatchStartControl.cs | 9 +++++---- .../OnlinePlay/Multiplayer/Match/CountdownButton.cs | 6 +----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index b09fd306b7..02a61208d4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Utils; @@ -79,7 +80,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -97,7 +98,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -150,7 +151,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); @@ -173,7 +174,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { - var popoverButton = this.ChildrenOfType().First(); + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); InputManager.MoveMouseTo(popoverButton); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs index e598f6670c..e37168bf25 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match foreach (var duration in available_delays) { - flow.Add(new PopoverButton + flow.Add(new OsuButton { RelativeSizeAxes = Axes.X, Text = $"Start match in {duration.Humanize()}", @@ -79,9 +79,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match return new OsuPopover { Child = flow }; } - - public class PopoverButton : OsuButton - { - } } } From 96a447f68bf753b9c9f914def65504c627c9a27c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 14:28:38 +0900 Subject: [PATCH 067/285] Rename Multiplayer prefix to button classes --- .../Visual/Multiplayer/QueueModeTestScene.cs | 4 +- .../TestSceneAllPlayersQueueMode.cs | 8 +-- .../Multiplayer/TestSceneMatchStartControl.cs | 60 +++++++++---------- .../TestSceneMultiplayerMatchSubScreen.cs | 2 +- .../TestSceneMultiplayerSpectateButton.cs | 2 +- .../Multiplayer/Match/MatchStartControl.cs | 6 +- ...utton.cs => MultiplayerCountdownButton.cs} | 4 +- ...adyButton.cs => MultiplayerReadyButton.cs} | 3 +- 8 files changed, 45 insertions(+), 44 deletions(-) rename osu.Game/Screens/OnlinePlay/Multiplayer/Match/{CountdownButton.cs => MultiplayerCountdownButton.cs} (95%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Match/{ReadyButton.cs => MultiplayerReadyButton.cs} (98%) diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index fd43674b3b..bafc579134 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -95,10 +95,10 @@ namespace osu.Game.Tests.Visual.Multiplayer protected void RunGameplay() { AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded); AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs index 582dacb332..0785315b26 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs @@ -102,10 +102,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); AddAssert("ruleset is correct", () => ((Player)CurrentScreen).Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); @@ -119,10 +119,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded); AddAssert("mods are correct", () => !((Player)CurrentScreen).Mods.Value.Any()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 02a61208d4..4fd6fd5d70 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -75,9 +75,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestStartWithCountdown() { - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); + AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); } @@ -93,9 +93,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestCancelCountdown() { - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); @@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); @@ -120,10 +120,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(2) }).WaitSafely()); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); } @@ -133,22 +133,22 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddAssert("countdown button is visible", () => this.ChildrenOfType().Single().IsPresent); - AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + AddAssert("countdown button is visible", () => this.ChildrenOfType().Single().IsPresent); + AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); - AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); - AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value); } [Test] public void TestSpectatingDuringCountdownWithNoReadyUsersCancelsCountdown() { - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); @@ -169,9 +169,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + ClickButtonWhenEnabled(); AddStep("click the first countdown button", () => { var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); @@ -182,7 +182,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddAssert("ready button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + AddAssert("ready button enabled", () => this.ChildrenOfType().Single().Enabled.Value); } [Test] @@ -200,7 +200,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID)); AddUntilStep("local user is host", () => MultiplayerClient.Room?.Host?.Equals(MultiplayerClient.LocalUser) == true); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null); } @@ -231,10 +231,10 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); } @@ -250,7 +250,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); verifyGameplayStartFlow(); @@ -265,7 +265,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.TransferHost(2); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0)); verifyGameplayStartFlow(); @@ -280,12 +280,12 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0)); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); AddUntilStep("ready button enabled", () => control.ChildrenOfType().Single().Enabled.Value); } @@ -305,7 +305,7 @@ namespace osu.Game.Tests.Visual.Multiplayer if (!isHost) AddStep("transfer host", () => MultiplayerClient.TransferHost(2)); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddRepeatStep("change user ready state", () => { @@ -323,7 +323,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void verifyGameplayStartFlow() { AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); AddStep("finish gameplay", () => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 850a115f4c..057032c413 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for spectating user state", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); AddUntilStep("match started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 07ac580276..13917f4eb0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -146,6 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer => AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); private void assertReadyButtonEnablement(bool shouldBeEnabled) - => AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => startControl.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); + => AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => startControl.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index 9470fb1d68..af7ed9b9e2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private Sample sampleUnready; private readonly BindableBool enabled = new BindableBool(); - private readonly CountdownButton countdownButton; + private readonly MultiplayerCountdownButton countdownButton; private int countReady; private ScheduledDelegate readySampleDelegate; private IBindable operationInProgress; @@ -50,14 +50,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { new Drawable[] { - new ReadyButton + new MultiplayerReadyButton { RelativeSizeAxes = Axes.Both, Size = Vector2.One, Action = onReadyClick, Enabled = { BindTarget = enabled }, }, - countdownButton = new CountdownButton + countdownButton = new MultiplayerCountdownButton { RelativeSizeAxes = Axes.Y, Size = new Vector2(40, 1), diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs similarity index 95% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs index e37168bf25..3bf7e91a55 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/CountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs @@ -18,7 +18,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class CountdownButton : IconButton, IHasPopover + public class MultiplayerCountdownButton : IconButton, IHasPopover { private static readonly TimeSpan[] available_delays = { @@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private readonly Drawable background; - public CountdownButton() + public MultiplayerCountdownButton() { Icon = FontAwesome.Solid.CaretDown; IconScale = new Vector2(0.6f); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs similarity index 98% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 007e055d8c..6ff717d5c3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -10,10 +10,11 @@ using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Online.Multiplayer; +using osu.Game.Screens.OnlinePlay.Components; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class ReadyButton : Components.ReadyButton + public class MultiplayerReadyButton : ReadyButton { public new Triangles Triangles => base.Triangles; From d36944ac950d86761931befc4a7e020040d9ce1e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 14:35:03 +0900 Subject: [PATCH 068/285] Dispose token manually Cover more branches with cancellation source disposal --- .../Visual/Multiplayer/TestMultiplayerClient.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 1f20292437..5fa2ca8890 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -343,19 +343,19 @@ namespace osu.Game.Tests.Visual.Multiplayer Schedule(() => { - if (Room.Countdown != countdown) - return; - - Room.Countdown = null; - MatchEvent(new CountdownChangedEvent { Countdown = null }); - using (cancellationSource) { + if (Room.Countdown != countdown) + return; + + Room.Countdown = null; + MatchEvent(new CountdownChangedEvent { Countdown = null }); + if (stopSource.Token.IsCancellationRequested) return; - } - StartMatch().WaitSafely(); + StartMatch().WaitSafely(); + } }); } From 8f3a4df70adc38f9ed4fb89e31b3367012eb8e45 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 14:44:27 +0900 Subject: [PATCH 069/285] Add explanation for try-catch --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 5fa2ca8890..f96ba8ba24 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -339,6 +339,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } catch (OperationCanceledException) { + // Clients need to be notified of cancellations in the following code. } Schedule(() => From d2ecc100e5390460a4216ac5e4ccc54ce62f89aa Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 15:07:01 +0900 Subject: [PATCH 070/285] Revert unnecessary async change --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index f96ba8ba24..7562b1d0e2 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -387,7 +387,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } } - public override async Task StartMatch() + public override Task StartMatch() { Debug.Assert(Room != null); @@ -395,7 +395,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad); - await ((IMultiplayerClient)this).LoadRequested(); + return ((IMultiplayerClient)this).LoadRequested(); } public override Task AbortGameplay() From f0d132b16e04e85bb84c8b13d42d15559d2ce4e5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 15:21:46 +0900 Subject: [PATCH 071/285] Rename FinishCountdown() -> SkipToEndOfCountdown() --- .../Visual/Multiplayer/TestSceneMatchStartControl.cs | 6 +++--- .../Visual/Multiplayer/TestMultiplayerClient.cs | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 4fd6fd5d70..a374488306 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); - AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); + AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); } @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); - AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); + AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); } @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - AddStep("finish countdown", () => MultiplayerClient.FinishCountdown()); + AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddUntilStep("match not started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.Open); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 7562b1d0e2..ea991af914 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -296,11 +296,15 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } - private CancellationTokenSource? countdownFinishSource; + private CancellationTokenSource? countdownSkipSource; private CancellationTokenSource? countdownStopSource; private Task countdownTask = Task.CompletedTask; - public void FinishCountdown() => countdownFinishSource?.Cancel(); + /// + /// Skips to the end of the currently-running countdown, if one is running, + /// and runs the callback (e.g. to start the match) as soon as possible unless the countdown has been cancelled. + /// + public void SkipToEndOfCountdown() => countdownSkipSource?.Cancel(); public override async Task SendMatchRequest(MatchUserRequest request) { @@ -313,8 +317,8 @@ namespace osu.Game.Tests.Visual.Multiplayer countdownStopSource?.Cancel(); var stopSource = countdownStopSource = new CancellationTokenSource(); - var finishSource = countdownFinishSource = new CancellationTokenSource(); - var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, finishSource.Token); + var skipSource = countdownSkipSource = new CancellationTokenSource(); + var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token); var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration }; Task lastCountdownTask = countdownTask; From 4c0d76573c10a0ae54463d491367ce43c0e3a691 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 15:51:30 +0900 Subject: [PATCH 072/285] Asserate code is running on update thread --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index ea991af914..6f57d818a4 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Extensions; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; @@ -314,6 +315,8 @@ namespace osu.Game.Tests.Visual.Multiplayer switch (request) { case StartMatchCountdownRequest matchCountdownRequest: + Debug.Assert(ThreadSafety.IsUpdateThread); + countdownStopSource?.Cancel(); var stopSource = countdownStopSource = new CancellationTokenSource(); From a7d5f2281ce59c004991070865a6c4cf640bc637 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 15:49:13 +0900 Subject: [PATCH 073/285] Apply beatmap offsets to legacy replay frame handling --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 8 +++++++- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index fefee370b9..9885fe5528 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -23,6 +23,8 @@ namespace osu.Game.Scoring.Legacy private IBeatmap currentBeatmap; private Ruleset currentRuleset; + private float beatmapOffset; + public Score Parse(Stream stream) { var score = new Score @@ -72,6 +74,10 @@ namespace osu.Game.Scoring.Legacy currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo; + // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) + // As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing. + beatmapOffset = currentBeatmap.BeatmapInfo.BeatmapVersion < 5 ? 24 : 0; + /* score.HpGraphString = */ sr.ReadString(); @@ -229,7 +235,7 @@ namespace osu.Game.Scoring.Legacy private void readLegacyReplay(Replay replay, StreamReader reader) { - float lastTime = 0; + float lastTime = beatmapOffset; ReplayFrame currentFrame = null; string[] frames = reader.ReadToEnd().Split(','); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index f0ead05280..6a321bed7a 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -111,6 +111,10 @@ namespace osu.Game.Scoring.Legacy { StringBuilder replayData = new StringBuilder(); + // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) + // As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing. + double offset = beatmap?.BeatmapInfo.BeatmapVersion < 5 ? -24 : 0; + if (score.Replay != null) { int lastTime = 0; @@ -120,7 +124,7 @@ namespace osu.Game.Scoring.Legacy var legacyFrame = getLegacyFrame(f); // Rounding because stable could only parse integral values - int time = (int)Math.Round(legacyFrame.Time); + int time = (int)Math.Round(legacyFrame.Time + offset); replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},")); lastTime = time; } From dfa076c16983fe5badebd5bc3887cdf81d2fe2fb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 16:29:59 +0900 Subject: [PATCH 074/285] Refactor cancellation logic --- .../Multiplayer/TestMultiplayerClient.cs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 6f57d818a4..9be1b18062 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -319,9 +319,10 @@ namespace osu.Game.Tests.Visual.Multiplayer countdownStopSource?.Cancel(); + // Note that this will leak CTSs, however this is a test method and we haven't noticed foregoing disposal of non-linked CTSs to be detrimental. + // If necessary, this can be moved into the final schedule below, and the class-level fields be nulled out accordingly. var stopSource = countdownStopSource = new CancellationTokenSource(); var skipSource = countdownSkipSource = new CancellationTokenSource(); - var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token); var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration }; Task lastCountdownTask = countdownTask; @@ -342,7 +343,8 @@ namespace osu.Game.Tests.Visual.Multiplayer try { - await Task.Delay(matchCountdownRequest.Duration, cancellationSource.Token).ConfigureAwait(false); + using (var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token)) + await Task.Delay(matchCountdownRequest.Duration, cancellationSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -351,19 +353,16 @@ namespace osu.Game.Tests.Visual.Multiplayer Schedule(() => { - using (cancellationSource) - { - if (Room.Countdown != countdown) - return; + if (Room.Countdown != countdown) + return; - Room.Countdown = null; - MatchEvent(new CountdownChangedEvent { Countdown = null }); + Room.Countdown = null; + MatchEvent(new CountdownChangedEvent { Countdown = null }); - if (stopSource.Token.IsCancellationRequested) - return; + if (stopSource.IsCancellationRequested) + return; - StartMatch().WaitSafely(); - } + StartMatch().WaitSafely(); }); } From 59a7eb532203629e8dbd6dbc8f44cbc83c03fbde Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 16:34:21 +0900 Subject: [PATCH 075/285] Add test coverage ensuring offsets are correct before and after legacy replay encode --- .../Formats/LegacyScoreDecoderTest.cs | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 2ba8c51a10..a50cef238a 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -8,6 +8,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; @@ -64,6 +65,55 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [TestCase(3)] + [TestCase(6)] + [TestCase(LegacyBeatmapDecoder.LATEST_VERSION)] + public void TestLegacyBeatmapReplayOffsets(int beatmapVersion) + { + const double first_frame_time = 2000; + const double second_frame_time = 3000; + + var ruleset = new OsuRuleset().RulesetInfo; + var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); + var beatmap = new TestBeatmap(ruleset) + { + BeatmapInfo = + { + BeatmapVersion = beatmapVersion + } + }; + + var score = new Score + { + ScoreInfo = scoreInfo, + Replay = new Replay + { + Frames = new List + { + new OsuReplayFrame(first_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton), + new OsuReplayFrame(second_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton) + } + } + }; + + // the "se" culture is used here, as it encodes the negative number sign as U+2212 MINUS SIGN, + // rather than the classic ASCII U+002D HYPHEN-MINUS. + CultureInfo.CurrentCulture = new CultureInfo("se"); + + var encodeStream = new MemoryStream(); + + var encoder = new LegacyScoreEncoder(score, beatmap); + encoder.Encode(encodeStream); + + var decodeStream = new MemoryStream(encodeStream.GetBuffer()); + + var decoder = new TestLegacyScoreDecoder(beatmapVersion); + var decodedAfterEncode = decoder.Parse(decodeStream); + + Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(first_frame_time)); + Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time)); + } + [Test] public void TestCultureInvariance() { @@ -118,6 +168,8 @@ namespace osu.Game.Tests.Beatmaps.Formats private class TestLegacyScoreDecoder : LegacyScoreDecoder { + private readonly int beatmapVersion; + private static readonly Dictionary rulesets = new Ruleset[] { new OsuRuleset(), @@ -126,6 +178,11 @@ namespace osu.Game.Tests.Beatmaps.Formats new ManiaRuleset() }.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID); + public TestLegacyScoreDecoder(int beatmapVersion = LegacyBeatmapDecoder.LATEST_VERSION) + { + this.beatmapVersion = beatmapVersion; + } + protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId]; protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap @@ -134,7 +191,8 @@ namespace osu.Game.Tests.Beatmaps.Formats { MD5Hash = md5Hash, Ruleset = new OsuRuleset().RulesetInfo, - Difficulty = new BeatmapDifficulty() + Difficulty = new BeatmapDifficulty(), + BeatmapVersion = beatmapVersion, } }); } From 2efae031c97a904c7876f6ee89093725b70f1bc5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 16:39:56 +0900 Subject: [PATCH 076/285] Add test coverage of decode specifically --- .../Formats/LegacyScoreDecoderTest.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index a50cef238a..39586bcd8c 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -65,10 +65,29 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [TestCase(3, true)] + [TestCase(6, false)] + [TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)] + public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied) + { + const double first_frame_time = 48; + const double second_frame_time = 65; + + var decoder = new TestLegacyScoreDecoder(beatmapVersion); + + using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr")) + { + var score = decoder.Parse(resourceStream); + + Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? 24 : 0))); + Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? 24 : 0))); + } + } + [TestCase(3)] [TestCase(6)] [TestCase(LegacyBeatmapDecoder.LATEST_VERSION)] - public void TestLegacyBeatmapReplayOffsets(int beatmapVersion) + public void TestLegacyBeatmapReplayOffsetsEncodeDecode(int beatmapVersion) { const double first_frame_time = 2000; const double second_frame_time = 3000; From a7554dcdf77637543960f712cbbcb521fc3f410b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 16:43:41 +0900 Subject: [PATCH 077/285] Use a constant for the early version timing offset --- osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs | 4 ++-- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 8 ++++++-- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 3 +-- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 8 ++++---- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 39586bcd8c..1cd910789f 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -79,8 +79,8 @@ namespace osu.Game.Tests.Beatmaps.Formats { var score = decoder.Parse(resourceStream); - Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? 24 : 0))); - Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? 24 : 0))); + Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0))); + Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0))); } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index e2a043490f..79d8bd3bb3 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -19,6 +19,11 @@ namespace osu.Game.Beatmaps.Formats { public class LegacyBeatmapDecoder : LegacyDecoder { + /// + /// An offset which needs to be applied to old beatmaps (v4 and lower) to correct timing changes that were applied at a game client level. + /// + public const int EARLY_VERSION_TIMING_OFFSET = 24; + internal static RulesetStore RulesetStore; private Beatmap beatmap; @@ -50,8 +55,7 @@ namespace osu.Game.Beatmaps.Formats RulesetStore = new AssemblyRulesetStore(); } - // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) - offset = FormatVersion < 5 ? 24 : 0; + offset = FormatVersion < 5 ? EARLY_VERSION_TIMING_OFFSET : 0; } protected override Beatmap CreateTemplateObject() diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 9885fe5528..754ace82c5 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -74,9 +74,8 @@ namespace osu.Game.Scoring.Legacy currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo; - // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) // As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing. - beatmapOffset = currentBeatmap.BeatmapInfo.BeatmapVersion < 5 ? 24 : 0; + beatmapOffset = currentBeatmap.BeatmapInfo.BeatmapVersion < 5 ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0; /* score.HpGraphString = */ sr.ReadString(); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 6a321bed7a..ae9afbf32e 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.IO; using System.Linq; using System.Text; using osu.Framework.Extensions; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; using osu.Game.IO.Legacy; using osu.Game.Replays.Legacy; @@ -14,8 +17,6 @@ using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; -#nullable enable - namespace osu.Game.Scoring.Legacy { public class LegacyScoreEncoder @@ -111,9 +112,8 @@ namespace osu.Game.Scoring.Legacy { StringBuilder replayData = new StringBuilder(); - // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) // As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing. - double offset = beatmap?.BeatmapInfo.BeatmapVersion < 5 ? -24 : 0; + double offset = beatmap?.BeatmapInfo.BeatmapVersion < 5 ? -LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0; if (score.Replay != null) { From d6fc53579eb0d7d5d3eed86575006b8820d9135e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 17:00:49 +0900 Subject: [PATCH 078/285] Split out shared code for encode-decode cycle (and remove unrelated culture set) --- .../Formats/LegacyScoreDecoderTest.cs | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 1cd910789f..1474f2d277 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -115,19 +115,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } }; - // the "se" culture is used here, as it encodes the negative number sign as U+2212 MINUS SIGN, - // rather than the classic ASCII U+002D HYPHEN-MINUS. - CultureInfo.CurrentCulture = new CultureInfo("se"); - - var encodeStream = new MemoryStream(); - - var encoder = new LegacyScoreEncoder(score, beatmap); - encoder.Encode(encodeStream); - - var decodeStream = new MemoryStream(encodeStream.GetBuffer()); - - var decoder = new TestLegacyScoreDecoder(beatmapVersion); - var decodedAfterEncode = decoder.Parse(decodeStream); + var decodedAfterEncode = encodeThenDecode(beatmapVersion, score, beatmap); Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(first_frame_time)); Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time)); @@ -155,15 +143,7 @@ namespace osu.Game.Tests.Beatmaps.Formats // rather than the classic ASCII U+002D HYPHEN-MINUS. CultureInfo.CurrentCulture = new CultureInfo("se"); - var encodeStream = new MemoryStream(); - - var encoder = new LegacyScoreEncoder(score, beatmap); - encoder.Encode(encodeStream); - - var decodeStream = new MemoryStream(encodeStream.GetBuffer()); - - var decoder = new TestLegacyScoreDecoder(); - var decodedAfterEncode = decoder.Parse(decodeStream); + var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap); Assert.Multiple(() => { @@ -179,6 +159,20 @@ namespace osu.Game.Tests.Beatmaps.Formats }); } + private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap) + { + var encodeStream = new MemoryStream(); + + var encoder = new LegacyScoreEncoder(score, beatmap); + encoder.Encode(encodeStream); + + var decodeStream = new MemoryStream(encodeStream.GetBuffer()); + + var decoder = new TestLegacyScoreDecoder(beatmapVersion); + var decodedAfterEncode = decoder.Parse(decodeStream); + return decodedAfterEncode; + } + [TearDown] public void TearDown() { From 528ffea38db1b368b49a5c203fb744d33cbb6568 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 17:11:08 +0900 Subject: [PATCH 079/285] Fix incorrect event binding --- .../OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 6ff717d5c3..746e4257f1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.LoadComplete(); - multiplayerClient.RoomUpdated += () => Scheduler.AddOnce(onRoomUpdated); + multiplayerClient.RoomUpdated += onRoomUpdated; onRoomUpdated(); } @@ -39,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private DateTimeOffset countdownReceivedTime; private ScheduledDelegate countdownUpdateDelegate; - private void onRoomUpdated() + private void onRoomUpdated() => Scheduler.AddOnce(() => { if (countdown == null && room?.Countdown != null) countdownReceivedTime = DateTimeOffset.Now; @@ -56,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match updateButtonText(); updateButtonColour(); - } + }); private void updateButtonText() { From e3f8bc05883e126e176d6b12e54e0df39c85003a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 17:14:51 +0900 Subject: [PATCH 080/285] Revert `Availability` to `private` --- .../Screens/OnlinePlay/Components/ReadyButton.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 79cf5c7236..cdaa39d2be 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -14,18 +14,20 @@ namespace osu.Game.Screens.OnlinePlay.Components public abstract class ReadyButton : TriangleButton, IHasTooltip { public new readonly BindableBool Enabled = new BindableBool(); - protected readonly IBindable Availability = new Bindable(); + + private readonly IBindable availability = new Bindable(); [BackgroundDependencyLoader] private void load(OnlinePlayBeatmapAvailabilityTracker beatmapTracker) { - Availability.BindTo(beatmapTracker.Availability); - Availability.BindValueChanged(_ => updateState()); + availability.BindTo(beatmapTracker.Availability); + + availability.BindValueChanged(_ => updateState()); Enabled.BindValueChanged(_ => updateState(), true); } private void updateState() => - base.Enabled.Value = Availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value; + base.Enabled.Value = availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value; public virtual LocalisableString TooltipText { @@ -34,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Components if (Enabled.Value) return string.Empty; - if (Availability.Value.State != DownloadState.LocallyAvailable) + if (availability.Value.State != DownloadState.LocallyAvailable) return "Beatmap not downloaded"; return string.Empty; From e889d9344159d6c38641d6eaae56c4ab957a34d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 17:47:58 +0900 Subject: [PATCH 081/285] Add asserts of playlist being non-empty after client operations --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 2d5496c5c1..faa995ed19 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -171,6 +171,8 @@ namespace osu.Game.Online.Multiplayer Room = joinedRoom; APIRoom = room; + Debug.Assert(joinedRoom.Playlist.Count > 0); + APIRoom.Playlist.Clear(); APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem)); @@ -683,6 +685,8 @@ namespace osu.Game.Online.Multiplayer Room.Playlist.Remove(Room.Playlist.Single(existing => existing.ID == playlistItemId)); APIRoom.Playlist.RemoveAll(existing => existing.ID == playlistItemId); + Debug.Assert(Room.Playlist.Count > 0); + ItemRemoved?.Invoke(playlistItemId); RoomUpdated?.Invoke(); }); From 2d58feebb1ff4050f522caeb053ce4ca819cdfb7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 17:54:21 +0900 Subject: [PATCH 082/285] Guard against potential null `CurrentItem` in `ParticipantPanel` --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 96a665f33d..128b9a1640 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -189,7 +189,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants var currentItem = Playlist.GetCurrentItem(); Debug.Assert(currentItem != null); - var ruleset = rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance(); + var ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null; int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; From ea9de0199d98ad4b4cfc654dba90af780eb96e88 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 20:09:32 +0900 Subject: [PATCH 083/285] Split countdown start into separate method --- .../Multiplayer/TestMultiplayerClient.cs | 106 +++++++++--------- 1 file changed, 55 insertions(+), 51 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 9be1b18062..16da64c61e 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -315,57 +315,7 @@ namespace osu.Game.Tests.Visual.Multiplayer switch (request) { case StartMatchCountdownRequest matchCountdownRequest: - Debug.Assert(ThreadSafety.IsUpdateThread); - - countdownStopSource?.Cancel(); - - // Note that this will leak CTSs, however this is a test method and we haven't noticed foregoing disposal of non-linked CTSs to be detrimental. - // If necessary, this can be moved into the final schedule below, and the class-level fields be nulled out accordingly. - var stopSource = countdownStopSource = new CancellationTokenSource(); - var skipSource = countdownSkipSource = new CancellationTokenSource(); - var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration }; - - Task lastCountdownTask = countdownTask; - countdownTask = start(); - - async Task start() - { - await lastCountdownTask; - - Schedule(() => - { - if (stopSource.IsCancellationRequested) - return; - - Room.Countdown = countdown; - MatchEvent(new CountdownChangedEvent { Countdown = countdown }); - }); - - try - { - using (var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token)) - await Task.Delay(matchCountdownRequest.Duration, cancellationSource.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Clients need to be notified of cancellations in the following code. - } - - Schedule(() => - { - if (Room.Countdown != countdown) - return; - - Room.Countdown = null; - MatchEvent(new CountdownChangedEvent { Countdown = null }); - - if (stopSource.IsCancellationRequested) - return; - - StartMatch().WaitSafely(); - }); - } - + startCountdown(new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration }, StartMatch); break; case StopCountdownRequest _: @@ -393,6 +343,60 @@ namespace osu.Game.Tests.Visual.Multiplayer } } + private void startCountdown(MultiplayerCountdown countdown, Func continuation) + { + Debug.Assert(Room != null); + Debug.Assert(ThreadSafety.IsUpdateThread); + + countdownStopSource?.Cancel(); + + // Note that this will leak CTSs, however this is a test method and we haven't noticed foregoing disposal of non-linked CTSs to be detrimental. + // If necessary, this can be moved into the final schedule below, and the class-level fields be nulled out accordingly. + var stopSource = countdownStopSource = new CancellationTokenSource(); + var skipSource = countdownSkipSource = new CancellationTokenSource(); + + Task lastCountdownTask = countdownTask; + countdownTask = start(); + + async Task start() + { + await lastCountdownTask; + + Schedule(() => + { + if (stopSource.IsCancellationRequested) + return; + + Room.Countdown = countdown; + MatchEvent(new CountdownChangedEvent { Countdown = countdown }); + }); + + try + { + using (var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token)) + await Task.Delay(countdown.TimeRemaining, cancellationSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Clients need to be notified of cancellations in the following code. + } + + Schedule(() => + { + if (Room.Countdown != countdown) + return; + + Room.Countdown = null; + MatchEvent(new CountdownChangedEvent { Countdown = null }); + + if (stopSource.IsCancellationRequested) + return; + + continuation().WaitSafely(); + }); + } + } + public override Task StartMatch() { Debug.Assert(Room != null); From 209de886ce3677482a57066efe76d312bc8813ce Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 20:13:58 +0900 Subject: [PATCH 084/285] Add method to stop countdown --- .../Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 16da64c61e..ef5936198b 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Multiplayer shouldStopCountdown |= Room.Host?.State != MultiplayerUserState.Ready && Room.Host?.State != MultiplayerUserState.Spectating; if (shouldStopCountdown) - countdownStopSource?.Cancel(); + stopCountdown(); break; case MultiplayerRoomState.WaitingForLoad: @@ -319,10 +319,7 @@ namespace osu.Game.Tests.Visual.Multiplayer break; case StopCountdownRequest _: - countdownStopSource?.Cancel(); - - Room.Countdown = null; - await MatchEvent(new CountdownChangedEvent { Countdown = Room.Countdown }); + stopCountdown(); break; case ChangeTeamRequest changeTeam: @@ -348,7 +345,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Debug.Assert(Room != null); Debug.Assert(ThreadSafety.IsUpdateThread); - countdownStopSource?.Cancel(); + stopCountdown(); // Note that this will leak CTSs, however this is a test method and we haven't noticed foregoing disposal of non-linked CTSs to be detrimental. // If necessary, this can be moved into the final schedule below, and the class-level fields be nulled out accordingly. @@ -397,6 +394,8 @@ namespace osu.Game.Tests.Visual.Multiplayer } } + private void stopCountdown() => countdownStopSource?.Cancel(); + public override Task StartMatch() { Debug.Assert(Room != null); From 5fe46a73cb5e075db8a1b330923011989b5404af Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 20:16:43 +0900 Subject: [PATCH 085/285] Update room state in more situations --- .../Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index ef5936198b..5c16c189d6 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -119,6 +119,12 @@ namespace osu.Game.Tests.Visual.Multiplayer Debug.Assert(Room != null); ((IMultiplayerClient)this).UserStateChanged(userId, newState); + updateRoomStateIfRequired(); + } + + private void updateRoomStateIfRequired() + { + Debug.Assert(Room != null); Schedule(() => { @@ -263,6 +269,7 @@ namespace osu.Game.Tests.Visual.Multiplayer ChangeUserState(user.UserID, MultiplayerUserState.Idle); await changeMatchType(settings.MatchType).ConfigureAwait(false); + updateRoomStateIfRequired(); } public override Task ChangeState(MultiplayerUserState newState) @@ -430,6 +437,7 @@ namespace osu.Game.Tests.Visual.Multiplayer await addItem(item).ConfigureAwait(false); await updateCurrentItem(Room).ConfigureAwait(false); + updateRoomStateIfRequired(); } public override Task AddPlaylistItem(MultiplayerPlaylistItem item) => AddUserPlaylistItem(api.LocalUser.Value.OnlineID, item); @@ -486,6 +494,7 @@ namespace osu.Game.Tests.Visual.Multiplayer await ((IMultiplayerClient)this).PlaylistItemRemoved(playlistItemId).ConfigureAwait(false); await updateCurrentItem(Room).ConfigureAwait(false); + updateRoomStateIfRequired(); } public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, playlistItemId); From f80692f342f703dd7f998bab4e7ad531362e4f37 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 20:19:05 +0900 Subject: [PATCH 086/285] Add missing TestMultiplayerClient coverage --- .../Multiplayer/TestMultiplayerClient.cs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 5c16c189d6..7b3e8ac3fe 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -125,6 +125,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void updateRoomStateIfRequired() { Debug.Assert(Room != null); + Debug.Assert(APIRoom != null); Schedule(() => { @@ -132,13 +133,28 @@ namespace osu.Game.Tests.Visual.Multiplayer { case MultiplayerRoomState.Open: // If there are no remaining ready users or the host is not ready, stop any existing countdown. - // Todo: When we have an "automatic start" mode, this should also start a new countdown if any users _are_ ready. // Todo: This doesn't yet support non-match-start countdowns. - bool shouldStopCountdown = Room.Users.All(u => u.State != MultiplayerUserState.Ready); - shouldStopCountdown |= Room.Host?.State != MultiplayerUserState.Ready && Room.Host?.State != MultiplayerUserState.Spectating; + if (Room.Settings.AutoStartDuration != TimeSpan.Zero) + { + bool shouldHaveCountdown = !APIRoom.Playlist.GetCurrentItem()!.Expired && Room.Users.Any(u => u.State == MultiplayerUserState.Ready); + + if (shouldHaveCountdown) + { + if (Room.Countdown == null) + startCountdown(new MatchStartCountdown { TimeRemaining = Room.Settings.AutoStartDuration }, StartMatch); + } + else + stopCountdown(); + } + else + { + bool shouldStopCountdown = Room.Users.All(u => u.State != MultiplayerUserState.Ready); + shouldStopCountdown |= Room.Host?.State != MultiplayerUserState.Ready && Room.Host?.State != MultiplayerUserState.Spectating; + + if (shouldStopCountdown) + stopCountdown(); + } - if (shouldStopCountdown) - stopCountdown(); break; case MultiplayerRoomState.WaitingForLoad: From 0d88af19ae1c0f726aa6a68b6b0e29bcc53e4dd5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 20:23:58 +0900 Subject: [PATCH 087/285] Fix local setting not being updated --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index de17c012eb..2f328a9339 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -744,6 +744,7 @@ namespace osu.Game.Online.Multiplayer APIRoom.Password.Value = Room.Settings.Password; APIRoom.Type.Value = Room.Settings.MatchType; APIRoom.QueueMode.Value = Room.Settings.QueueMode; + APIRoom.AutoStartDuration.Value = Room.Settings.AutoStartDuration; RoomUpdated?.Invoke(); } From d3a957d64e154410da41203875c3a243d0a505ab Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 20:24:09 +0900 Subject: [PATCH 088/285] Fix test client not receiving initial value --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 7b3e8ac3fe..a4c63d762e 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -226,7 +226,8 @@ namespace osu.Game.Tests.Visual.Multiplayer Name = apiRoom.Name.Value, MatchType = apiRoom.Type.Value, Password = password, - QueueMode = apiRoom.QueueMode.Value + QueueMode = apiRoom.QueueMode.Value, + AutoStartDuration = apiRoom.AutoStartDuration.Value }, Playlist = serverSidePlaylist.ToList(), Users = { localUser }, From 1abf3f7bf48515d6f910a09387c021d58eb962ee Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 24 Mar 2022 20:25:17 +0900 Subject: [PATCH 089/285] Fix cancel tooltip showing for autostart countdowns --- .../OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 746e4257f1..79d974fd3a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -168,7 +168,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { get { - if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready) + if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready && room.Settings.AutoStartDuration == TimeSpan.Zero) return "Cancel countdown"; return base.TooltipText; From 2938f44e6c0307293b4b2f32c26d7f352a5dfe4b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 23:40:46 +0900 Subject: [PATCH 090/285] Update `PresentExternally` usages in line with framework changes --- osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs | 2 +- osu.Game/IO/WrappedStorage.cs | 4 ++-- osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs index 93cfa9634e..f0aa857769 100644 --- a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs +++ b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tournament.Screens.Setup dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true); Action = () => game.GracefullyExit(); - folderButton.Action = storage.PresentExternally; + folderButton.Action = () => storage.PresentExternally(); ButtonText = "Close osu!"; } diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs index 6f0f898de3..a6605de1d2 100644 --- a/osu.Game/IO/WrappedStorage.cs +++ b/osu.Game/IO/WrappedStorage.cs @@ -70,9 +70,9 @@ namespace osu.Game.IO public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) => UnderlyingStorage.GetStream(MutatePath(path), access, mode); - public override void OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename)); + public override bool OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename)); - public override void PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename)); + public override bool PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename)); public override Storage GetStorageForDirectory(string path) { diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 158d8811b5..0b4eca6379 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -68,7 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Add(new SettingsButton { Text = GeneralSettingsStrings.OpenOsuFolder, - Action = storage.PresentExternally, + Action = () => storage.PresentExternally(), }); Add(new SettingsButton From b04ca111c6b4f526dc38f172e81275fc7641db3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 22:28:26 +0900 Subject: [PATCH 091/285] Allow realm subscriptions to be initiated from a non-update thread --- osu.Game/Database/RealmAccess.cs | 30 +++++++++++++++++------------- osu.Game/OsuGameBase.cs | 2 +- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index f0d4011ab8..8574002436 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; using System.ComponentModel; @@ -17,6 +19,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; +using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Input.Bindings; @@ -28,8 +31,6 @@ using osu.Game.Stores; using Realms; using Realms.Exceptions; -#nullable enable - namespace osu.Game.Database { /// @@ -46,6 +47,8 @@ namespace osu.Game.Database private readonly IDatabaseContextFactory? efContextFactory; + private readonly SynchronizationContext? updateThreadSyncContext; + /// /// Version history: /// 6 ~2021-10-18 First tracked version. @@ -143,12 +146,15 @@ namespace osu.Game.Database /// /// The game storage which will be used to create the realm backing file. /// The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified. + /// The game update thread, used to post realm operations into a thread-safe context. /// An EF factory used only for migration purposes. - public RealmAccess(Storage storage, string filename, IDatabaseContextFactory? efContextFactory = null) + public RealmAccess(Storage storage, string filename, GameThread? updateThread = null, IDatabaseContextFactory? efContextFactory = null) { this.storage = storage; this.efContextFactory = efContextFactory; + updateThreadSyncContext = updateThread?.SynchronizationContext ?? SynchronizationContext.Current; + Filename = filename; if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) @@ -379,9 +385,6 @@ namespace osu.Game.Database public IDisposable RegisterForNotifications(Func> query, NotificationCallbackDelegate callback) where T : RealmObjectBase { - if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread."); - lock (realmLock) { Func action = realm => query(realm).QueryAsyncWithNotifications(callback); @@ -459,23 +462,24 @@ namespace osu.Game.Database /// An which should be disposed to unsubscribe any inner subscription. public IDisposable RegisterCustomSubscription(Func action) { - if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread."); - - var syncContext = SynchronizationContext.Current; + if (updateThreadSyncContext == null) + throw new InvalidOperationException("Attempted to register a realm subscription before update thread registration."); total_subscriptions.Value++; - registerSubscription(action); + if (ThreadSafety.IsUpdateThread) + updateThreadSyncContext.Send(_ => registerSubscription(action), null); + else + updateThreadSyncContext.Post(_ => registerSubscription(action), null); // This token is returned to the consumer. // When disposed, it will cause the registration to be permanently ceased (unsubscribed with realm and unregistered by this class). return new InvokeOnDisposal(() => { if (ThreadSafety.IsUpdateThread) - syncContext.Send(_ => unsubscribe(), null); + updateThreadSyncContext.Send(_ => unsubscribe(), null); else - syncContext.Post(_ => unsubscribe(), null); + updateThreadSyncContext.Post(_ => unsubscribe(), null); void unsubscribe() { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5468db348e..7b9aca4086 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -200,7 +200,7 @@ namespace osu.Game if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME)) dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage)); - dependencies.Cache(realm = new RealmAccess(Storage, "client", EFContextFactory)); + dependencies.Cache(realm = new RealmAccess(Storage, "client", Host.UpdateThread, EFContextFactory)); dependencies.CacheAs(RulesetStore = new RealmRulesetStore(realm, Storage)); dependencies.CacheAs(RulesetStore); From 878e8d21a3925bd4fc1cf4339ca6b346643bbac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 24 Mar 2022 21:51:10 +0100 Subject: [PATCH 092/285] Remove assertion to fix "expression always true" inspection --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 128b9a1640..70f4030b79 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -187,8 +186,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; var currentItem = Playlist.GetCurrentItem(); - Debug.Assert(currentItem != null); - var ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null; int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null; From b4c0155b3d22f2115fc250ef3451db9e1b2fe273 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 13:07:21 +0900 Subject: [PATCH 093/285] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 1b5461959a..182495cd56 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1c1deaae8e..4193dc8fa0 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 23101c5af6..f97f15d091 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - + From 09c5325b08bfa8bb9c993c3428b5d79e65c91ed2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 13:18:49 +0900 Subject: [PATCH 094/285] Update resources --- 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 182495cd56..6a3b113fa2 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4193dc8fa0..3c01f29671 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index f97f15d091..c8f170497d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -62,7 +62,7 @@ - + From 816fcae3a182647781490aa3c817e93a1c223f47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 15:12:39 +0900 Subject: [PATCH 095/285] Rename `Button` to `MainMenuButton` to differentiate better --- osu.Game/Screens/Menu/ButtonSystem.cs | 28 +++++++++---------- .../Menu/{Button.cs => MainMenuButton.cs} | 4 +-- 2 files changed, 16 insertions(+), 16 deletions(-) rename osu.Game/Screens/Menu/{Button.cs => MainMenuButton.cs} (97%) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index b03425fef4..a1f0d22efc 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -79,10 +79,10 @@ namespace osu.Game.Screens.Menu private readonly ButtonArea buttonArea; - private readonly Button backButton; + private readonly MainMenuButton backButton; - private readonly List - public class Button : BeatSyncedContainer, IStateful + public class MainMenuButton : BeatSyncedContainer, IStateful { public event Action StateChanged; @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Menu public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos); - public Button(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown) + public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown) { this.sampleName = sampleName; this.clickAction = clickAction; From 416b57ea69ac8c6bf43c7d0b52338a65246dc2a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 15:13:22 +0900 Subject: [PATCH 096/285] Fix main menu buttons handling keys when super (cmd) is held --- osu.Game/Screens/Menu/MainMenuButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index 1014249531..88bea43b23 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -209,7 +209,7 @@ namespace osu.Game.Screens.Menu protected override bool OnKeyDown(KeyDownEvent e) { - if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed) + if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed) return false; if (TriggerKey == e.Key && TriggerKey != Key.Unknown) From de4c04ef809c7b0ccdc4ea8e7ca248a665875ad8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 15:34:33 +0900 Subject: [PATCH 097/285] Fix non-matching json propert for `AutoStartDuration` --- osu.Game/Online/Rooms/Room.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 76bac44242..60c0503ddd 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -95,7 +95,7 @@ namespace osu.Game.Online.Rooms [Cached] public readonly Bindable AutoStartDuration = new Bindable(); - [JsonProperty("start_duration")] + [JsonProperty("auto_start_duration")] private ushort autoStartDuration { get => (ushort)AutoStartDuration.Value.TotalSeconds; From b1f0f89fdde94a7e3beb4821d2c7145af463ac34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 15:41:01 +0900 Subject: [PATCH 098/285] Simplify `AutoStart` and `Host` checks --- .../Online/Multiplayer/MultiplayerRoomSettings.cs | 2 ++ .../Multiplayer/Match/MatchStartControl.cs | 12 ++++++------ .../Multiplayer/Match/MultiplayerReadyButton.cs | 2 +- .../Visual/Multiplayer/TestMultiplayerClient.cs | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index f8b238d61b..3aed42147b 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -31,6 +31,8 @@ namespace osu.Game.Online.Multiplayer [Key(5)] public TimeSpan AutoStartDuration { get; set; } + public bool AutoStartEnabled => AutoStartDuration != TimeSpan.Zero; + public bool Equals(MultiplayerRoomSettings? other) { if (ReferenceEquals(this, other)) return true; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index 78719ac878..6297203684 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match clickOperation = ongoingOperationTracker.BeginOperation(); // Ensure the current user becomes ready before being able to do anything else (start match, stop countdown, unready). - if (!isReady() || !Client.IsHost || Room.Settings.AutoStartDuration != TimeSpan.Zero) + if (!isReady() || !Client.IsHost || Room.Settings.AutoStartEnabled) { toggleReady(); return; @@ -172,19 +172,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating); - if (Room.Countdown != null || Room.Settings.AutoStartDuration != TimeSpan.Zero) - countdownButton.Alpha = 0; + if (!Client.IsHost || Room.Countdown != null || Room.Settings.AutoStartEnabled) + countdownButton.Hide(); else { switch (localUser?.State) { default: - countdownButton.Alpha = 0; + countdownButton.Hide(); break; case MultiplayerUserState.Spectating: case MultiplayerUserState.Ready: - countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0; + countdownButton.Show(); break; } } @@ -197,7 +197,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match // When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready. if (localUser?.State == MultiplayerUserState.Spectating) - enabled.Value &= Room.Host?.Equals(localUser) == true && newCountReady > 0; + enabled.Value &= Client.IsHost && newCountReady > 0; 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 79d974fd3a..71107095c2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -168,7 +168,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { get { - if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready && room.Settings.AutoStartDuration == TimeSpan.Zero) + if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready && !room.Settings.AutoStartEnabled) return "Cancel countdown"; return base.TooltipText; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index a4c63d762e..6e8749d655 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Multiplayer case MultiplayerRoomState.Open: // If there are no remaining ready users or the host is not ready, stop any existing countdown. // Todo: This doesn't yet support non-match-start countdowns. - if (Room.Settings.AutoStartDuration != TimeSpan.Zero) + if (Room.Settings.AutoStartEnabled) { bool shouldHaveCountdown = !APIRoom.Playlist.GetCurrentItem()!.Expired && Room.Users.Any(u => u.State == MultiplayerUserState.Ready); From 792e79265beb2bbdc82a004267acff9a322774e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 15:46:27 +0900 Subject: [PATCH 099/285] Add ignore rule for new helper property --- osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index 3aed42147b..5c086066e6 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -31,6 +31,7 @@ namespace osu.Game.Online.Multiplayer [Key(5)] public TimeSpan AutoStartDuration { get; set; } + [IgnoreMember] public bool AutoStartEnabled => AutoStartDuration != TimeSpan.Zero; public bool Equals(MultiplayerRoomSettings? other) From 23c4f9910e5bba1c0b556bb3838d9eea87cdd6dd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 25 Mar 2022 15:53:55 +0900 Subject: [PATCH 100/285] Apply notnull constraint --- osu.Game/Skinning/ISkin.cs | 4 +++- osu.Game/Skinning/LegacySkin.cs | 6 +++--- osu.Game/Skinning/ResourceStoreBackedSkin.cs | 5 ++++- osu.Game/Skinning/Skin.cs | 4 +++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs index 4b14dcfd62..414a316dec 100644 --- a/osu.Game/Skinning/ISkin.cs +++ b/osu.Game/Skinning/ISkin.cs @@ -52,6 +52,8 @@ namespace osu.Game.Skinning /// /// The requested configuration value. /// A matching value boxed in an , or null if unavailable. - IBindable? GetConfig(TLookup lookup); + IBindable? GetConfig(TLookup lookup) + where TLookup : notnull + where TValue : notnull; } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index a3c08f4ba2..92713023f4 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -282,6 +282,7 @@ namespace osu.Game.Skinning => source.ImageLookups.TryGetValue(lookup, out string image) ? new Bindable(image) : null; private IBindable? legacySettingLookup(SkinConfiguration.LegacySetting legacySetting) + where TValue : notnull { switch (legacySetting) { @@ -294,10 +295,9 @@ namespace osu.Game.Skinning } private IBindable? genericLookup(TLookup lookup) + where TLookup : notnull + where TValue : notnull { - if (lookup == null) - return null; - try { if (Configuration.ConfigDictionary.TryGetValue(lookup.ToString(), out string val)) diff --git a/osu.Game/Skinning/ResourceStoreBackedSkin.cs b/osu.Game/Skinning/ResourceStoreBackedSkin.cs index 4787b5a4e9..48286bff59 100644 --- a/osu.Game/Skinning/ResourceStoreBackedSkin.cs +++ b/osu.Game/Skinning/ResourceStoreBackedSkin.cs @@ -46,7 +46,10 @@ namespace osu.Game.Skinning return null; } - public IBindable? GetConfig(TLookup lookup) => null; + public IBindable? GetConfig(TLookup lookup) + where TLookup : notnull + where TValue : notnull + => null; public void Dispose() { diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 2766e58c96..324ed90a67 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -50,7 +50,9 @@ namespace osu.Game.Skinning public abstract Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT); - public abstract IBindable? GetConfig(TLookup lookup); + public abstract IBindable? GetConfig(TLookup lookup) + where TLookup : notnull + where TValue : notnull; /// /// Construct a new skin. From e44db4e726784b846c5d7b3b3213bfd59a8e16f6 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 25 Mar 2022 15:13:25 +0800 Subject: [PATCH 101/285] Revert unintentional behavior change of random mod Actually, using OsuPlayfield.BASE_SIZE.Y makes a touch more sense since it is the short side of the playfield, but I guess it is better to preserve replays than to introduce pointless breaking changes. --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 3c2c5d7759..fea9246035 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -48,12 +48,12 @@ namespace osu.Game.Rulesets.Osu.Mods if (positionInfo == positionInfos.First()) { - positionInfo.DistanceFromPrevious = (float)rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2; + positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2); positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); } else { - positionInfo.RelativeAngle = (float)(rateOfChangeMultiplier * 2 * Math.PI * Math.Min(1f, positionInfo.DistanceFromPrevious / (playfield_diagonal * 0.5f))); + positionInfo.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, positionInfo.DistanceFromPrevious / (playfield_diagonal * 0.5f)); } } From 4a30b6ef565f320074116cb7ea4a9b447818e471 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 16:22:53 +0900 Subject: [PATCH 102/285] Update multiplayer countdown button text more often At once a second, it regularly skips whole seconds (because scheduler isn't guaranteed to run exactly as often as specified). 10 updates a second seems amicable and less noticeable to my eye. --- .../OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 746e4257f1..f0049565c9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match countdown = room?.Countdown; if (room?.Countdown != null) - countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 1000, true); + countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 100, true); else { countdownUpdateDelegate?.Cancel(); From 5fcd3b07f1352601ec8970a891f431f9d70433f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 16:35:32 +0900 Subject: [PATCH 103/285] Fix visual test crashes due to local realm not having update thread --- osu.Game/Tests/Visual/OsuTestScene.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index f287a04d71..6c332c2408 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -115,11 +115,13 @@ namespace osu.Game.Tests.Visual protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - headlessHostStorage = (parent.Get() as HeadlessGameHost)?.Storage; + var host = parent.Get(); + + headlessHostStorage = (host as HeadlessGameHost)?.Storage; Resources = parent.Get().Resources; - realm = new Lazy(() => new RealmAccess(LocalStorage, "client")); + realm = new Lazy(() => new RealmAccess(LocalStorage, "client", host.UpdateThread)); RecycleLocalStorage(false); From 6b22e5774f79d7e7b19e0ff24aee61b77ddb8b9e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 16:42:35 +0900 Subject: [PATCH 104/285] Remove conditional access on known non-null --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index b4da91a97a..2e1ca09fe4 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual }, new OsuSpriteText { - Text = skin?.SkinInfo?.Value.Name ?? "none", + Text = skin?.SkinInfo.Value.Name ?? "none", Scale = new Vector2(1.5f), Padding = new MarginPadding(5), }, From 2553cfed75bd40c79d0b13bb1d61af2c7413b177 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 25 Mar 2022 17:04:45 +0900 Subject: [PATCH 105/285] Match new server ready/unready logic --- .../Visual/Multiplayer/TestMultiplayerClient.cs | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 6e8749d655..b9304f713d 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -138,21 +138,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { bool shouldHaveCountdown = !APIRoom.Playlist.GetCurrentItem()!.Expired && Room.Users.Any(u => u.State == MultiplayerUserState.Ready); - if (shouldHaveCountdown) - { - if (Room.Countdown == null) - startCountdown(new MatchStartCountdown { TimeRemaining = Room.Settings.AutoStartDuration }, StartMatch); - } - else - stopCountdown(); - } - else - { - bool shouldStopCountdown = Room.Users.All(u => u.State != MultiplayerUserState.Ready); - shouldStopCountdown |= Room.Host?.State != MultiplayerUserState.Ready && Room.Host?.State != MultiplayerUserState.Spectating; - - if (shouldStopCountdown) - stopCountdown(); + if (shouldHaveCountdown && Room.Countdown == null) + startCountdown(new MatchStartCountdown { TimeRemaining = Room.Settings.AutoStartDuration }, StartMatch); } break; From d5a37c223711bd7a9be258b27840c74a57dd619a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 25 Mar 2022 17:06:04 +0900 Subject: [PATCH 106/285] Remove irrelevant test --- .../Multiplayer/TestSceneMatchStartControl.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index a374488306..f0a2a28b34 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -143,26 +143,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value); } - [Test] - public void TestSpectatingDuringCountdownWithNoReadyUsersCancelsCountdown() - { - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); - AddStep("click the first countdown button", () => - { - var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); - InputManager.MoveMouseTo(popoverButton); - InputManager.Click(MouseButton.Left); - }); - - AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); - AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - - AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); - AddUntilStep("match not started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.Open); - } - [Test] public void TestReadyButtonEnabledWhileSpectatingDuringCountdown() { From c1649714f4784384b1de945f570e9ea79541a6e5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 25 Mar 2022 17:12:00 +0900 Subject: [PATCH 107/285] Add a few more ready button tests --- .../Multiplayer/TestSceneMatchStartControl.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index f0a2a28b34..e58b9893ce 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -185,6 +185,31 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null); } + [Test] + public void TestCountdownButtonVisibilityWithAutoStartEnablement() + { + ClickButtonWhenEnabled(); + AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); + AddUntilStep("countdown button visible", () => this.ChildrenOfType().Single().IsPresent); + + AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) })); + + ClickButtonWhenEnabled(); + AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); + AddUntilStep("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); + } + + [Test] + public void TestClickingReadyButtonUnReadiesDuringAutoStart() + { + AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) })); + ClickButtonWhenEnabled(); + AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); + + ClickButtonWhenEnabled(); + AddUntilStep("local user became idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); + } + [Test] public void TestDeletedBeatmapDisableReady() { From 463091bde25e9d97353c6ea3bcb978b1b0523f79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 18:25:32 +0900 Subject: [PATCH 108/285] Use more appropriate icon on countdown button (and give tooltip) --- .../Multiplayer/Match/MultiplayerCountdownButton.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs index 3bf7e91a55..d6a09e0b31 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs @@ -34,8 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public MultiplayerCountdownButton() { - Icon = FontAwesome.Solid.CaretDown; - IconScale = new Vector2(0.6f); + Icon = FontAwesome.Regular.Clock; Add(background = new Box { @@ -44,6 +43,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }); base.Action = this.ShowPopover; + + TooltipText = "Countdown settings"; } [BackgroundDependencyLoader] From 3ad092d80879975b371698f9c4dd8e510f2d8227 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 25 Mar 2022 18:29:00 +0900 Subject: [PATCH 109/285] Always show the countdown button when host --- .../Multiplayer/Match/MatchStartControl.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index 6297203684..fa1b682779 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private Sample sampleReadyAll; private Sample sampleUnready; - private readonly BindableBool enabled = new BindableBool(); + private readonly MultiplayerReadyButton readyButton; private readonly MultiplayerCountdownButton countdownButton; private int countReady; private ScheduledDelegate readySampleDelegate; @@ -50,12 +50,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { new Drawable[] { - new MultiplayerReadyButton + readyButton = new MultiplayerReadyButton { RelativeSizeAxes = Axes.Both, Size = Vector2.One, Action = onReadyClick, - Enabled = { BindTarget = enabled }, }, countdownButton = new MultiplayerCountdownButton { @@ -63,7 +62,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Size = new Vector2(40, 1), Alpha = 0, Action = startCountdown, - Enabled = { BindTarget = enabled } } } } @@ -163,7 +161,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { if (Room == null) { - enabled.Value = false; + readyButton.Enabled.Value = false; + countdownButton.Enabled.Value = false; return; } @@ -172,7 +171,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating); - if (!Client.IsHost || Room.Countdown != null || Room.Settings.AutoStartEnabled) + if (!Client.IsHost || Room.Settings.AutoStartEnabled) countdownButton.Hide(); else { @@ -182,6 +181,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match countdownButton.Hide(); break; + case MultiplayerUserState.Idle: case MultiplayerUserState.Spectating: case MultiplayerUserState.Ready: countdownButton.Show(); @@ -189,15 +189,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } } - enabled.Value = + readyButton.Enabled.Value = countdownButton.Enabled.Value = Room.State == MultiplayerRoomState.Open && CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId && !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired && !operationInProgress.Value; - // When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready. + // When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready. if (localUser?.State == MultiplayerUserState.Spectating) - enabled.Value &= Client.IsHost && newCountReady > 0; + readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0; if (newCountReady == countReady) return; From a0a3bba46e82f954c2264e9553a24c160d9fa813 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 18:31:23 +0900 Subject: [PATCH 110/285] Avoid crashing if a skin component cannot be instantiated correctly --- osu.Game/Skinning/Skin.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index e00dd950a7..7e93a74b7e 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -144,9 +144,23 @@ namespace osu.Game.Skinning if (!DrawableComponentInfo.TryGetValue(target.Target, out var skinnableInfo)) return null; + var components = new List(); + + foreach (var i in skinnableInfo) + { + try + { + components.Add(i.CreateInstance()); + } + catch (Exception e) + { + Logger.Error(e, $"Unable to create skin component {i.Type.Name}"); + } + } + return new SkinnableTargetComponentsContainer { - ChildrenEnumerable = skinnableInfo.Select(i => i.CreateInstance()) + Children = components, }; } From b13408aed0b07708ffdb0c54bb4545da38074638 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 18:37:20 +0900 Subject: [PATCH 111/285] Add back "room visibility" control commented out for now --- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index aef04c106d..a103d71120 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -165,6 +165,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match LengthLimit = 100, }, }, + // new Section("Room visibility") + // { + // Alpha = disabled_alpha, + // Child = AvailabilityPicker = new RoomAvailabilityPicker + // { + // Enabled = { Value = false } + // }, + // }, new Section("Game type") { Child = new FillFlowContainer From 320110f179efd7f7e4b5e565d5e85156852c04ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 18:37:46 +0900 Subject: [PATCH 112/285] Remove "room visibility" from playlists settings to match --- .../Playlists/PlaylistsRoomSettingsOverlay.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 6674a37c3c..1bd227fa4d 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -152,14 +152,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists PlaceholderText = "Unlimited", }, }, - new Section("Room visibility") - { - Alpha = disabled_alpha, - Child = AvailabilityPicker = new RoomAvailabilityPicker - { - Enabled = { Value = false } - }, - }, + // new Section("Room visibility") + // { + // Alpha = disabled_alpha, + // Child = AvailabilityPicker = new RoomAvailabilityPicker + // { + // Enabled = { Value = false } + // }, + // }, new Section("Max participants") { Alpha = disabled_alpha, From 76abce486766094622979c2b4be641f8feec118b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 18:38:26 +0900 Subject: [PATCH 113/285] Add missing wait calls on `async` test steps --- .../Visual/Multiplayer/TestSceneMatchStartControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index e58b9893ce..52854db235 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -192,7 +192,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); AddUntilStep("countdown button visible", () => this.ChildrenOfType().Single().IsPresent); - AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) })); + AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely()); ClickButtonWhenEnabled(); AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); @@ -202,7 +202,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestClickingReadyButtonUnReadiesDuringAutoStart() { - AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) })); + AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely()); ClickButtonWhenEnabled(); AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); From 9963efce517068bbb7232374097ac88e8be1979d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 25 Mar 2022 18:40:32 +0900 Subject: [PATCH 114/285] Improve ready/countdown button UX --- .../Multiplayer/Match/MatchStartControl.cs | 32 ++++++++----------- .../Match/MultiplayerCountdownButton.cs | 23 +++++++++++++ 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index fa1b682779..1201279929 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -62,6 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Size = new Vector2(40, 1), Alpha = 0, Action = startCountdown, + CancelAction = cancelCountdown } } } @@ -106,30 +107,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Debug.Assert(clickOperation == null); clickOperation = ongoingOperationTracker.BeginOperation(); - // Ensure the current user becomes ready before being able to do anything else (start match, stop countdown, unready). - if (!isReady() || !Client.IsHost || Room.Settings.AutoStartEnabled) - { + if (isReady() && Client.IsHost && Room.Countdown == null) + startMatch(); + else toggleReady(); - return; - } - - // Local user is the room host and is in a ready state. - // The only action they can take is to stop a countdown if one's currently running. - if (Room.Countdown != null) - { - stopCountdown(); - return; - } - - // And if a countdown isn't running, start the match. - startMatch(); bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating; void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation()); - void stopCountdown() => Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation()); - void startMatch() => Client.StartMatch().ContinueWith(t => { // accessing Exception here silences any potential errors from the antecedent task @@ -151,6 +137,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation()); } + private void cancelCountdown() + { + Debug.Assert(clickOperation == null); + clickOperation = ongoingOperationTracker.BeginOperation(); + + Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation()); + } + private void endOperation() { clickOperation?.Dispose(); @@ -197,7 +191,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match // When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready. if (localUser?.State == MultiplayerUserState.Spectating) - readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0; + readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && Room.Countdown == null; if (newCountReady == countReady) return; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs index 3bf7e91a55..d1e28fb5e0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.Multiplayer; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match @@ -29,6 +30,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }; public new Action Action; + public Action CancelAction; + + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } + + [Resolved] + private OsuColour colours { get; set; } private readonly Drawable background; @@ -77,6 +85,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }); } + if (multiplayerClient.Room?.Countdown != null && multiplayerClient.IsHost) + { + flow.Add(new OsuButton + { + RelativeSizeAxes = Axes.X, + Text = "Cancel", + BackgroundColour = colours.Red, + Action = () => + { + CancelAction(); + this.HidePopover(); + } + }); + } + return new OsuPopover { Child = flow }; } } From f989158a31e8c8c5a2e9301075c8b1fa4da0cae2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 19:20:16 +0900 Subject: [PATCH 115/285] Add back playlist availability control (because it's hooked up half way?) --- .../Playlists/PlaylistsRoomSettingsOverlay.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 1bd227fa4d..6674a37c3c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -152,14 +152,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists PlaceholderText = "Unlimited", }, }, - // new Section("Room visibility") - // { - // Alpha = disabled_alpha, - // Child = AvailabilityPicker = new RoomAvailabilityPicker - // { - // Enabled = { Value = false } - // }, - // }, + new Section("Room visibility") + { + Alpha = disabled_alpha, + Child = AvailabilityPicker = new RoomAvailabilityPicker + { + Enabled = { Value = false } + }, + }, new Section("Max participants") { Alpha = disabled_alpha, From 0146717fcb9bda32c892657d8030edde92774c16 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Mar 2022 14:29:31 +0900 Subject: [PATCH 116/285] Adjust test button sizing to better match actual usage --- osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 52854db235..b0a451f1d0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(200, 50), + Size = new Vector2(250, 50), } }; }); From 5725cc36ff27b315f4246460a72366701e271637 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Mar 2022 14:29:51 +0900 Subject: [PATCH 117/285] Add animation to countdown button when countdown is active --- .../Match/MultiplayerCountdownButton.cs | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs index 7f819d9e75..c84fcff11e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerCountdownButton.cs @@ -30,6 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }; public new Action Action; + public Action CancelAction; [Resolved] @@ -61,6 +62,38 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match background.Colour = colours.Green; } + protected override void LoadComplete() + { + base.LoadComplete(); + + multiplayerClient.RoomUpdated += onRoomUpdated; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + multiplayerClient.RoomUpdated -= onRoomUpdated; + } + + private void onRoomUpdated() => Scheduler.AddOnce(() => + { + bool countdownActive = multiplayerClient.Room?.Countdown != null; + + if (countdownActive) + { + background + .FadeColour(colours.YellowLight, 100, Easing.In) + .Then() + .FadeColour(colours.YellowDark, 900, Easing.OutQuint) + .Loop(); + } + else + { + background + .FadeColour(colours.Green, 200, Easing.OutQuint); + } + }); + public Popover GetPopover() { var flow = new FillFlowContainer @@ -77,7 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { RelativeSizeAxes = Axes.X, Text = $"Start match in {duration.Humanize()}", - BackgroundColour = background.Colour, + BackgroundColour = colours.Green, Action = () => { Action(duration); @@ -91,7 +124,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match flow.Add(new OsuButton { RelativeSizeAxes = Axes.X, - Text = "Cancel", + Text = "Stop countdown", BackgroundColour = colours.Red, Action = () => { From c3a0f0d6b8bbb5a13387ac84c98abd9431d1651d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Mar 2022 14:43:41 +0900 Subject: [PATCH 118/285] Update tests in line with new button behaviour --- .../Multiplayer/TestSceneMatchStartControl.cs | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index b0a451f1d0..5c2fd26857 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -85,7 +85,6 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - AddAssert("countdown button not visible", () => !this.ChildrenOfType().Single().IsPresent); AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); } @@ -103,7 +102,13 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - ClickButtonWhenEnabled(); + ClickButtonWhenEnabled(); + AddStep("click the cancel button", () => + { + var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().Last(); + InputManager.MoveMouseTo(popoverButton); + InputManager.Click(MouseButton.Left); + }); AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); @@ -128,43 +133,21 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] - public void TestCountdownButtonEnablementAndVisibilityWhileSpectating() + public void TestCountdownWhileSpectating() { AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); AddAssert("countdown button is visible", () => this.ChildrenOfType().Single().IsPresent); - AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value); AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); - AddAssert("countdown button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value); AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); AddAssert("countdown button enabled", () => this.ChildrenOfType().Single().Enabled.Value); } - [Test] - public void TestReadyButtonEnabledWhileSpectatingDuringCountdown() - { - AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); - AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); - - ClickButtonWhenEnabled(); - AddUntilStep("countdown button shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - ClickButtonWhenEnabled(); - AddStep("click the first countdown button", () => - { - var popoverButton = this.ChildrenOfType().Single().ChildrenOfType().First(); - InputManager.MoveMouseTo(popoverButton); - InputManager.Click(MouseButton.Left); - }); - - AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); - AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); - - AddAssert("ready button enabled", () => this.ChildrenOfType().Single().Enabled.Value); - } - [Test] public void TestBecomeHostDuringCountdownAndReady() { From 0d8a7246dd6e6093c0d467eb2fc0f283d9f1a67b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Mar 2022 16:35:53 +0900 Subject: [PATCH 119/285] Add basic implementation of clock --- .../Visual/Menus/TestSceneToolbarClock.cs | 64 +++++++ osu.Game/Overlays/Toolbar/Toolbar.cs | 1 + osu.Game/Overlays/Toolbar/ToolbarClock.cs | 165 ++++++++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs create mode 100644 osu.Game/Overlays/Toolbar/ToolbarClock.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs new file mode 100644 index 0000000000..56ac1b24f1 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Overlays.Toolbar; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Menus +{ + [TestFixture] + public class TestSceneToolbarClock : OsuManualInputManagerTestScene + { + public TestSceneToolbarClock() + { + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = Toolbar.HEIGHT, + Scale = new Vector2(4), + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new Box + { + Colour = Color4.DarkRed, + RelativeSizeAxes = Axes.Y, + Width = 2, + }, + new ToolbarClock(), + new Box + { + Colour = Color4.DarkRed, + RelativeSizeAxes = Axes.Y, + Width = 2, + }, + } + }, + } + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 776f7ad7b7..b7fb2e45be 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -104,6 +104,7 @@ namespace osu.Game.Overlays.Toolbar // Icon = FontAwesome.Solid.search //}, userButton = new ToolbarUserButton(), + new ToolbarClock(), new ToolbarNotificationButton(), } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs new file mode 100644 index 0000000000..b8dc7ccbbd --- /dev/null +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -0,0 +1,165 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Toolbar +{ + public class ToolbarClock : CompositeDrawable + { + public ToolbarClock() + { + RelativeSizeAxes = Axes.Y; + Width = 110; + + Padding = new MarginPadding(10); + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new AnalogDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new DigitalDisplay + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + } + }; + } + + private class DigitalDisplay : ClockDisplay + { + private OsuSpriteText text; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Y; + Width = 50; + + InternalChildren = new Drawable[] + { + text = new OsuSpriteText + { + Text = "00:00:00", + } + }; + } + + protected override void UpdateDisplay(DateTimeOffset now) + { + text.Text = $"{now:HH:mm:ss}"; + } + } + + private class AnalogDisplay : ClockDisplay + { + private Drawable hour; + private Drawable minute; + private Drawable second; + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(30); + + InternalChildren = new[] + { + new Circle + { + RelativeSizeAxes = Axes.Both, + }, + hour = new Hand + { + Colour = Color4.Orange, + Size = new Vector2(0.3f, 1.2f), + }, + minute = new Hand + { + Colour = Color4.Green, + Size = new Vector2(0.45f, 1), + }, + second = new Hand + { + Colour = Color4.Blue, + Size = new Vector2(0.48f, 0.5f), + }, + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1.5f), + Colour = Color4.Black, + }, + }; + } + + private class Hand : Circle + { + public Hand() + { + Anchor = Anchor.Centre; + Origin = Anchor.CentreLeft; + + RelativeSizeAxes = Axes.X; + } + } + + protected override void UpdateDisplay(DateTimeOffset now) + { + float secondFractional = now.Second / 60f; + float minuteFractional = (now.Minute + secondFractional) / 60f; + float hourFractional = ((minuteFractional + now.Hour) % 12) / 12f; + + updateRotation(hour, hourFractional); + updateRotation(minute, minuteFractional); + updateRotation(second, secondFractional); + } + + private void updateRotation(Drawable hand, float fraction) + { + const float duration = 320; + + float rotation = fraction * 360 - 90; + + if (Math.Abs(hand.Rotation - rotation) > 180) + hand.RotateTo(rotation); + else + hand.RotateTo(rotation, duration, Easing.OutElastic); + } + } + + private abstract class ClockDisplay : CompositeDrawable + { + private int lastSecond; + + protected override void Update() + { + base.Update(); + + var now = DateTimeOffset.Now; + + if (now.Second != lastSecond) + { + lastSecond = now.Second; + UpdateDisplay(now); + } + } + + protected abstract void UpdateDisplay(DateTimeOffset now); + } + } +} From e8f5a8e3d6cad69b1d55b66458ddbfc36fb9d7b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Mar 2022 17:22:48 +0900 Subject: [PATCH 120/285] Refactor hands for legibility (visual and code) --- osu.Game/Overlays/Toolbar/ToolbarClock.cs | 105 +++++++++++++++++----- 1 file changed, 82 insertions(+), 23 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs index b8dc7ccbbd..a3c2994ffc 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; using osuTK.Graphics; @@ -14,6 +15,8 @@ namespace osu.Game.Overlays.Toolbar { public class ToolbarClock : CompositeDrawable { + private const float hand_thickness = 2.2f; + public ToolbarClock() { RelativeSizeAxes = Axes.Y; @@ -72,7 +75,7 @@ namespace osu.Game.Overlays.Toolbar private Drawable second; [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { Size = new Vector2(30); @@ -82,38 +85,94 @@ namespace osu.Game.Overlays.Toolbar { RelativeSizeAxes = Axes.Both, }, - hour = new Hand - { - Colour = Color4.Orange, - Size = new Vector2(0.3f, 1.2f), - }, - minute = new Hand - { - Colour = Color4.Green, - Size = new Vector2(0.45f, 1), - }, - second = new Hand - { - Colour = Color4.Blue, - Size = new Vector2(0.48f, 0.5f), - }, - new Circle + hour = new LargeHand(0.3f), + minute = new LargeHand(0.45f), + second = new SecondHand(), + new CentreCircle { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(1.5f), - Colour = Color4.Black, - }, + } }; } - private class Hand : Circle + private class CentreCircle : CompositeDrawable { - public Hand() + public CentreCircle() + { + InternalChildren = new Drawable[] + { + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(hand_thickness), + Colour = Color4.Black, + }, + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(hand_thickness * 0.7f), + Colour = Color4.White, + }, + }; + } + } + + private class SecondHand : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.X; + Width = 0.54f; + + Height = hand_thickness / 2; + Anchor = Anchor.Centre; + Origin = Anchor.Custom; + + OriginPosition = new Vector2(Height * 2, Height / 2); + + InternalChildren = new Drawable[] + { + new Circle + { + Colour = colours.YellowDark, + RelativeSizeAxes = Axes.Both, + }, + }; + } + } + + private class LargeHand : CompositeDrawable + { + public LargeHand(float length) + { + Width = length; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) { Anchor = Anchor.Centre; Origin = Anchor.CentreLeft; + Origin = Anchor.Custom; + OriginPosition = new Vector2(hand_thickness / 2); // offset x also, to ensure the centre of the line is centered on the face. + Height = hand_thickness; + + InternalChildren = new Drawable[] + { + new Circle + { + Colour = colours.PurpleLight, + RelativeSizeAxes = Axes.Both, + BorderThickness = 0.5f, + BorderColour = colours.Purple, + }, + }; + RelativeSizeAxes = Axes.X; } } @@ -144,7 +203,7 @@ namespace osu.Game.Overlays.Toolbar private abstract class ClockDisplay : CompositeDrawable { - private int lastSecond; + private int? lastSecond; protected override void Update() { From 999ae88af2209af542acee1a55f38b80650fec0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Mar 2022 17:55:33 +0900 Subject: [PATCH 121/285] Show game time display in clock --- .../Visual/Menus/TestSceneToolbarClock.cs | 17 ++++++- osu.Game/Overlays/Toolbar/ToolbarClock.cs | 46 ++++++++++++------- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs index 56ac1b24f1..a8d25242ee 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Timing; using osu.Game.Overlays.Toolbar; using osuTK; using osuTK.Graphics; @@ -14,11 +15,13 @@ namespace osu.Game.Tests.Visual.Menus [TestFixture] public class TestSceneToolbarClock : OsuManualInputManagerTestScene { + private readonly Container mainContainer; + public TestSceneToolbarClock() { Children = new Drawable[] { - new Container + mainContainer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -60,5 +63,17 @@ namespace osu.Game.Tests.Visual.Menus }, }; } + + [Test] + public void TestRealGameTime() + { + AddStep("Set game time real", () => mainContainer.Clock = Clock); + } + + [Test] + public void TestLongGameTime() + { + AddStep("Set game time long", () => mainContainer.Clock = new FramedOffsetClock(Clock, false) { Offset = 3600.0 * 24 * 1000 * 98 }); + } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs index a3c2994ffc..f7a92ebbe6 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Toolbar public ToolbarClock() { RelativeSizeAxes = Axes.Y; - Width = 110; + AutoSizeAxes = Axes.X; Padding = new MarginPadding(10); } @@ -28,43 +28,55 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader] private void load() { - InternalChildren = new Drawable[] + InternalChild = new FillFlowContainer { - new AnalogDisplay + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - new DigitalDisplay - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, + new AnalogDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new DigitalDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } } }; } private class DigitalDisplay : ClockDisplay { - private OsuSpriteText text; + private OsuSpriteText realTime; + private OsuSpriteText gameTime; [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { AutoSizeAxes = Axes.Y; - Width = 50; + Width = 70; // Allows for space for game time up to 99 days. InternalChildren = new Drawable[] { - text = new OsuSpriteText + realTime = new OsuSpriteText(), + gameTime = new OsuSpriteText { - Text = "00:00:00", + Y = 14, + Colour = colours.PurpleLight, + Scale = new Vector2(0.6f) } }; } protected override void UpdateDisplay(DateTimeOffset now) { - text.Text = $"{now:HH:mm:ss}"; + realTime.Text = $"{now:HH:mm:ss}"; + gameTime.Text = $"running {new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}"; } } @@ -75,7 +87,7 @@ namespace osu.Game.Overlays.Toolbar private Drawable second; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { Size = new Vector2(30); From 252d1924e757331d23f2f6849e09497f864b5773 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Mar 2022 18:13:33 +0900 Subject: [PATCH 122/285] Adjust metrics for better legibility at small sizes --- osu.Game/Overlays/Toolbar/ToolbarClock.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs index f7a92ebbe6..3cbf07c875 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Toolbar RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; - Padding = new MarginPadding(10); + Padding = new MarginPadding(5); } [BackgroundDependencyLoader] @@ -59,7 +59,7 @@ namespace osu.Game.Overlays.Toolbar private void load(OsuColour colours) { AutoSizeAxes = Axes.Y; - Width = 70; // Allows for space for game time up to 99 days. + Width = 66; // Allows for space for game time up to 99 days (in the padding area since this is quite rare). InternalChildren = new Drawable[] { @@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader] private void load() { - Size = new Vector2(30); + Size = new Vector2(20); InternalChildren = new[] { From 622458e8570309fb23ccf9064ad573d3af6dbbda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Mar 2022 18:56:28 +0900 Subject: [PATCH 123/285] Allow scaling display in the test scene --- osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs index a8d25242ee..064d6f82fd 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs @@ -27,7 +27,6 @@ namespace osu.Game.Tests.Visual.Menus Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, Height = Toolbar.HEIGHT, - Scale = new Vector2(4), Children = new Drawable[] { new Box @@ -62,6 +61,8 @@ namespace osu.Game.Tests.Visual.Menus } }, }; + + AddSliderStep("scale", 0.5, 4, 1, scale => mainContainer.Scale = new Vector2((float)scale)); } [Test] From 97e9049a2c47945ad08f234216713abfc469b213 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Mar 2022 18:56:40 +0900 Subject: [PATCH 124/285] Adjust metrics and colours to better match the actual toolbar --- osu.Game/Overlays/Toolbar/ToolbarClock.cs | 40 ++++++++++++++--------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs index 3cbf07c875..1cbad6a471 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Toolbar { public class ToolbarClock : CompositeDrawable { - private const float hand_thickness = 2.2f; + private const float hand_thickness = 2.4f; public ToolbarClock() { @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Toolbar gameTime = new OsuSpriteText { Y = 14, - Colour = colours.PurpleLight, + Colour = colours.PinkLight, Scale = new Vector2(0.6f) } }; @@ -89,16 +89,25 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader] private void load() { - Size = new Vector2(20); + Size = new Vector2(22); InternalChildren = new[] { - new Circle + new CircularContainer { RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = 2, + BorderColour = Color4.White, + Child = new Box + { + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both + }, }, - hour = new LargeHand(0.3f), - minute = new LargeHand(0.45f), + hour = new LargeHand(0.34f), + minute = new LargeHand(0.48f), second = new SecondHand(), new CentreCircle { @@ -110,7 +119,8 @@ namespace osu.Game.Overlays.Toolbar private class CentreCircle : CompositeDrawable { - public CentreCircle() + [BackgroundDependencyLoader] + private void load(OsuColour colours) { InternalChildren = new Drawable[] { @@ -119,14 +129,14 @@ namespace osu.Game.Overlays.Toolbar Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(hand_thickness), - Colour = Color4.Black, + Colour = Color4.White, }, new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(hand_thickness * 0.7f), - Colour = Color4.White, + Colour = colours.PinkLight, }, }; } @@ -138,9 +148,9 @@ namespace osu.Game.Overlays.Toolbar private void load(OsuColour colours) { RelativeSizeAxes = Axes.X; - Width = 0.54f; + Width = 0.66f; - Height = hand_thickness / 2; + Height = hand_thickness * 0.7f; Anchor = Anchor.Centre; Origin = Anchor.Custom; @@ -150,7 +160,7 @@ namespace osu.Game.Overlays.Toolbar { new Circle { - Colour = colours.YellowDark, + Colour = colours.PinkLight, RelativeSizeAxes = Axes.Both, }, }; @@ -178,10 +188,10 @@ namespace osu.Game.Overlays.Toolbar { new Circle { - Colour = colours.PurpleLight, + Colour = Color4.White, RelativeSizeAxes = Axes.Both, - BorderThickness = 0.5f, - BorderColour = colours.Purple, + BorderThickness = 0.7f, + BorderColour = colours.Gray2, }, }; From 4ddf3cb1d9293d9e374a3c945bbfcb74d9fbe1c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Mar 2022 19:18:41 +0900 Subject: [PATCH 125/285] Add ability to cycle between clock display modes --- osu.Game/Configuration/OsuConfigManager.cs | 3 + .../Configuration/ToolbarClockDisplayMode.cs | 13 +++ osu.Game/Overlays/Toolbar/ToolbarClock.cs | 85 ++++++++++++++++++- 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Configuration/ToolbarClockDisplayMode.cs diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index c279ce1220..e8f13ba902 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -44,6 +44,8 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f); + SetDefault(OsuSetting.ToolbarClockDisplayMode, ToolbarClockDisplayMode.Full); + // Online settings SetDefault(OsuSetting.Username, string.Empty); SetDefault(OsuSetting.Token, string.Empty); @@ -295,6 +297,7 @@ namespace osu.Game.Configuration RandomSelectAlgorithm, ShowFpsDisplay, ChatDisplayHeight, + ToolbarClockDisplayMode, Version, ShowConvertedBeatmaps, Skin, diff --git a/osu.Game/Configuration/ToolbarClockDisplayMode.cs b/osu.Game/Configuration/ToolbarClockDisplayMode.cs new file mode 100644 index 0000000000..2f42f7a9b5 --- /dev/null +++ b/osu.Game/Configuration/ToolbarClockDisplayMode.cs @@ -0,0 +1,13 @@ +// 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.Configuration +{ + public enum ToolbarClockDisplayMode + { + Analog, + Digital, + DigitalWithRuntime, + Full + } +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs index 1cbad6a471..68772d8fc3 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -3,9 +3,12 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; @@ -15,6 +18,11 @@ namespace osu.Game.Overlays.Toolbar { public class ToolbarClock : CompositeDrawable { + private Bindable clockDisplayMode; + + private DigitalDisplay digital; + private AnalogDisplay analog; + private const float hand_thickness = 2.4f; public ToolbarClock() @@ -26,8 +34,10 @@ namespace osu.Game.Overlays.Toolbar } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { + clockDisplayMode = config.GetBindable(OsuSetting.ToolbarClockDisplayMode); + InternalChild = new FillFlowContainer { RelativeSizeAxes = Axes.Y, @@ -36,12 +46,12 @@ namespace osu.Game.Overlays.Toolbar Spacing = new Vector2(5), Children = new Drawable[] { - new AnalogDisplay + analog = new AnalogDisplay { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - new DigitalDisplay + digital = new DigitalDisplay { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -50,16 +60,75 @@ namespace osu.Game.Overlays.Toolbar }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + clockDisplayMode.BindValueChanged(displayMode => + { + bool showAnalog = displayMode.NewValue == ToolbarClockDisplayMode.Analog || displayMode.NewValue == ToolbarClockDisplayMode.Full; + bool showDigital = displayMode.NewValue != ToolbarClockDisplayMode.Analog; + bool showRuntime = displayMode.NewValue == ToolbarClockDisplayMode.DigitalWithRuntime || displayMode.NewValue == ToolbarClockDisplayMode.Full; + + digital.FadeTo(showDigital ? 1 : 0); + digital.ShowRuntime = showRuntime; + + analog.FadeTo(showAnalog ? 1 : 0); + }, true); + } + + protected override bool OnClick(ClickEvent e) + { + cycleDisplayMode(); + return true; + } + + private void cycleDisplayMode() + { + switch (clockDisplayMode.Value) + { + case ToolbarClockDisplayMode.Analog: + clockDisplayMode.Value = ToolbarClockDisplayMode.Full; + break; + + case ToolbarClockDisplayMode.Digital: + clockDisplayMode.Value = ToolbarClockDisplayMode.Analog; + break; + + case ToolbarClockDisplayMode.DigitalWithRuntime: + clockDisplayMode.Value = ToolbarClockDisplayMode.Digital; + break; + + case ToolbarClockDisplayMode.Full: + clockDisplayMode.Value = ToolbarClockDisplayMode.DigitalWithRuntime; + break; + } + } + private class DigitalDisplay : ClockDisplay { private OsuSpriteText realTime; private OsuSpriteText gameTime; + private bool showRuntime = true; + + public bool ShowRuntime + { + get => showRuntime; + set + { + if (showRuntime == value) + return; + + showRuntime = value; + updateMetrics(); + } + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { AutoSizeAxes = Axes.Y; - Width = 66; // Allows for space for game time up to 99 days (in the padding area since this is quite rare). InternalChildren = new Drawable[] { @@ -71,6 +140,8 @@ namespace osu.Game.Overlays.Toolbar Scale = new Vector2(0.6f) } }; + + updateMetrics(); } protected override void UpdateDisplay(DateTimeOffset now) @@ -78,6 +149,12 @@ namespace osu.Game.Overlays.Toolbar realTime.Text = $"{now:HH:mm:ss}"; gameTime.Text = $"running {new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}"; } + + private void updateMetrics() + { + Width = showRuntime ? 66 : 45; // Allows for space for game time up to 99 days (in the padding area since this is quite rare). + gameTime.FadeTo(showRuntime ? 1 : 0); + } } private class AnalogDisplay : ClockDisplay From ab8db3b7dc1c4b68cfbc2885ba03e1840096357e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Mar 2022 20:34:17 +0900 Subject: [PATCH 126/285] Move nested classes to own files --- .../Overlays/Toolbar/AnalogClockDisplay.cs | 159 ++++++++++++ osu.Game/Overlays/Toolbar/ClockDisplay.cs | 28 +++ .../Overlays/Toolbar/DigitalClockDisplay.cs | 64 +++++ osu.Game/Overlays/Toolbar/ToolbarClock.cs | 230 +----------------- 4 files changed, 255 insertions(+), 226 deletions(-) create mode 100644 osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs create mode 100644 osu.Game/Overlays/Toolbar/ClockDisplay.cs create mode 100644 osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs diff --git a/osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs b/osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs new file mode 100644 index 0000000000..e100ee0c06 --- /dev/null +++ b/osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs @@ -0,0 +1,159 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Toolbar +{ + public class AnalogClockDisplay : ClockDisplay + { + private const float hand_thickness = 2.4f; + + private Drawable hour; + private Drawable minute; + private Drawable second; + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(22); + + InternalChildren = new[] + { + new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = 2, + BorderColour = Color4.White, + Child = new Box + { + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both + }, + }, + hour = new LargeHand(0.34f), + minute = new LargeHand(0.48f), + second = new SecondHand(), + new CentreCircle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + } + + protected override void UpdateDisplay(DateTimeOffset now) + { + float secondFractional = now.Second / 60f; + float minuteFractional = (now.Minute + secondFractional) / 60f; + float hourFractional = ((minuteFractional + now.Hour) % 12) / 12f; + + updateRotation(hour, hourFractional); + updateRotation(minute, minuteFractional); + updateRotation(second, secondFractional); + } + + private void updateRotation(Drawable hand, float fraction) + { + const float duration = 320; + + float rotation = fraction * 360 - 90; + + if (Math.Abs(hand.Rotation - rotation) > 180) + hand.RotateTo(rotation); + else + hand.RotateTo(rotation, duration, Easing.OutElastic); + } + + private class CentreCircle : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + InternalChildren = new Drawable[] + { + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(hand_thickness), + Colour = Color4.White, + }, + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(hand_thickness * 0.7f), + Colour = colours.PinkLight, + }, + }; + } + } + + private class SecondHand : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.X; + Width = 0.66f; + + Height = hand_thickness * 0.7f; + Anchor = Anchor.Centre; + Origin = Anchor.Custom; + + OriginPosition = new Vector2(Height * 2, Height / 2); + + InternalChildren = new Drawable[] + { + new Circle + { + Colour = colours.PinkLight, + RelativeSizeAxes = Axes.Both, + }, + }; + } + } + + private class LargeHand : CompositeDrawable + { + public LargeHand(float length) + { + Width = length; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Anchor = Anchor.Centre; + Origin = Anchor.CentreLeft; + + Origin = Anchor.Custom; + OriginPosition = new Vector2(hand_thickness / 2); // offset x also, to ensure the centre of the line is centered on the face. + Height = hand_thickness; + + InternalChildren = new Drawable[] + { + new Circle + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + BorderThickness = 0.7f, + BorderColour = colours.Gray2, + }, + }; + + RelativeSizeAxes = Axes.X; + } + } + } +} diff --git a/osu.Game/Overlays/Toolbar/ClockDisplay.cs b/osu.Game/Overlays/Toolbar/ClockDisplay.cs new file mode 100644 index 0000000000..c1befbb198 --- /dev/null +++ b/osu.Game/Overlays/Toolbar/ClockDisplay.cs @@ -0,0 +1,28 @@ +// 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.Containers; + +namespace osu.Game.Overlays.Toolbar +{ + public abstract class ClockDisplay : CompositeDrawable + { + private int? lastSecond; + + protected override void Update() + { + base.Update(); + + var now = DateTimeOffset.Now; + + if (now.Second != lastSecond) + { + lastSecond = now.Second; + UpdateDisplay(now); + } + } + + protected abstract void UpdateDisplay(DateTimeOffset now); + } +} diff --git a/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs b/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs new file mode 100644 index 0000000000..090f8c4a0f --- /dev/null +++ b/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Overlays.Toolbar +{ + public class DigitalClockDisplay : ClockDisplay + { + private OsuSpriteText realTime; + private OsuSpriteText gameTime; + + private bool showRuntime = true; + + public bool ShowRuntime + { + get => showRuntime; + set + { + if (showRuntime == value) + return; + + showRuntime = value; + updateMetrics(); + } + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AutoSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + realTime = new OsuSpriteText(), + gameTime = new OsuSpriteText + { + Y = 14, + Colour = colours.PinkLight, + Scale = new Vector2(0.6f) + } + }; + + updateMetrics(); + } + + protected override void UpdateDisplay(DateTimeOffset now) + { + realTime.Text = $"{now:HH:mm:ss}"; + gameTime.Text = $"running {new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}"; + } + + private void updateMetrics() + { + Width = showRuntime ? 66 : 45; // Allows for space for game time up to 99 days (in the padding area since this is quite rare). + gameTime.FadeTo(showRuntime ? 1 : 0); + } + } +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs index 68772d8fc3..332bfdf638 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -1,18 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Configuration; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osuTK; -using osuTK.Graphics; namespace osu.Game.Overlays.Toolbar { @@ -20,10 +15,8 @@ namespace osu.Game.Overlays.Toolbar { private Bindable clockDisplayMode; - private DigitalDisplay digital; - private AnalogDisplay analog; - - private const float hand_thickness = 2.4f; + private DigitalClockDisplay digital; + private AnalogClockDisplay analog; public ToolbarClock() { @@ -46,12 +39,12 @@ namespace osu.Game.Overlays.Toolbar Spacing = new Vector2(5), Children = new Drawable[] { - analog = new AnalogDisplay + analog = new AnalogClockDisplay { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - digital = new DigitalDisplay + digital = new DigitalClockDisplay { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -104,220 +97,5 @@ namespace osu.Game.Overlays.Toolbar break; } } - - private class DigitalDisplay : ClockDisplay - { - private OsuSpriteText realTime; - private OsuSpriteText gameTime; - - private bool showRuntime = true; - - public bool ShowRuntime - { - get => showRuntime; - set - { - if (showRuntime == value) - return; - - showRuntime = value; - updateMetrics(); - } - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - AutoSizeAxes = Axes.Y; - - InternalChildren = new Drawable[] - { - realTime = new OsuSpriteText(), - gameTime = new OsuSpriteText - { - Y = 14, - Colour = colours.PinkLight, - Scale = new Vector2(0.6f) - } - }; - - updateMetrics(); - } - - protected override void UpdateDisplay(DateTimeOffset now) - { - realTime.Text = $"{now:HH:mm:ss}"; - gameTime.Text = $"running {new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}"; - } - - private void updateMetrics() - { - Width = showRuntime ? 66 : 45; // Allows for space for game time up to 99 days (in the padding area since this is quite rare). - gameTime.FadeTo(showRuntime ? 1 : 0); - } - } - - private class AnalogDisplay : ClockDisplay - { - private Drawable hour; - private Drawable minute; - private Drawable second; - - [BackgroundDependencyLoader] - private void load() - { - Size = new Vector2(22); - - InternalChildren = new[] - { - new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = 2, - BorderColour = Color4.White, - Child = new Box - { - AlwaysPresent = true, - Alpha = 0, - RelativeSizeAxes = Axes.Both - }, - }, - hour = new LargeHand(0.34f), - minute = new LargeHand(0.48f), - second = new SecondHand(), - new CentreCircle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } - }; - } - - private class CentreCircle : CompositeDrawable - { - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - InternalChildren = new Drawable[] - { - new Circle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(hand_thickness), - Colour = Color4.White, - }, - new Circle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(hand_thickness * 0.7f), - Colour = colours.PinkLight, - }, - }; - } - } - - private class SecondHand : CompositeDrawable - { - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - RelativeSizeAxes = Axes.X; - Width = 0.66f; - - Height = hand_thickness * 0.7f; - Anchor = Anchor.Centre; - Origin = Anchor.Custom; - - OriginPosition = new Vector2(Height * 2, Height / 2); - - InternalChildren = new Drawable[] - { - new Circle - { - Colour = colours.PinkLight, - RelativeSizeAxes = Axes.Both, - }, - }; - } - } - - private class LargeHand : CompositeDrawable - { - public LargeHand(float length) - { - Width = length; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Anchor = Anchor.Centre; - Origin = Anchor.CentreLeft; - - Origin = Anchor.Custom; - OriginPosition = new Vector2(hand_thickness / 2); // offset x also, to ensure the centre of the line is centered on the face. - Height = hand_thickness; - - InternalChildren = new Drawable[] - { - new Circle - { - Colour = Color4.White, - RelativeSizeAxes = Axes.Both, - BorderThickness = 0.7f, - BorderColour = colours.Gray2, - }, - }; - - RelativeSizeAxes = Axes.X; - } - } - - protected override void UpdateDisplay(DateTimeOffset now) - { - float secondFractional = now.Second / 60f; - float minuteFractional = (now.Minute + secondFractional) / 60f; - float hourFractional = ((minuteFractional + now.Hour) % 12) / 12f; - - updateRotation(hour, hourFractional); - updateRotation(minute, minuteFractional); - updateRotation(second, secondFractional); - } - - private void updateRotation(Drawable hand, float fraction) - { - const float duration = 320; - - float rotation = fraction * 360 - 90; - - if (Math.Abs(hand.Rotation - rotation) > 180) - hand.RotateTo(rotation); - else - hand.RotateTo(rotation, duration, Easing.OutElastic); - } - } - - private abstract class ClockDisplay : CompositeDrawable - { - private int? lastSecond; - - protected override void Update() - { - base.Update(); - - var now = DateTimeOffset.Now; - - if (now.Second != lastSecond) - { - lastSecond = now.Second; - UpdateDisplay(now); - } - } - - protected abstract void UpdateDisplay(DateTimeOffset now); - } } } From 9b7b897b3cd14508ff0bee285093480ba4dae9f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Mar 2022 20:34:50 +0900 Subject: [PATCH 127/285] Adjust padding to fit better --- osu.Game/Overlays/Toolbar/ToolbarClock.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs index 332bfdf638..ad5c9ac7a1 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -23,7 +23,7 @@ namespace osu.Game.Overlays.Toolbar RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; - Padding = new MarginPadding(5); + Padding = new MarginPadding(10); } [BackgroundDependencyLoader] From b6822f81909bea1f3fabd95e50dc9f50f410447a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 26 Mar 2022 14:55:19 +0300 Subject: [PATCH 128/285] Remove redundant/outdated `Origin` line --- osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs b/osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs index e100ee0c06..9228900e99 100644 --- a/osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs +++ b/osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs @@ -135,10 +135,10 @@ namespace osu.Game.Overlays.Toolbar private void load(OsuColour colours) { Anchor = Anchor.Centre; - Origin = Anchor.CentreLeft; - Origin = Anchor.Custom; + OriginPosition = new Vector2(hand_thickness / 2); // offset x also, to ensure the centre of the line is centered on the face. + Height = hand_thickness; InternalChildren = new Drawable[] From 638453131714b4bc5305fd9a7fa50447b2edd07f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 27 Mar 2022 01:39:31 +0900 Subject: [PATCH 129/285] Fix LogoVisualisation draw thread safety --- osu.Game/Screens/Menu/LogoVisualisation.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 4acd73bfa0..34f1d46067 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -176,7 +176,7 @@ namespace osu.Game.Screens.Menu private static readonly Color4 transparent_white = Color4.White.Opacity(0.2f); - private float[] audioData; + private readonly float[] audioData = new float[256]; private readonly QuadBatch vertexBatch = new QuadBatch(100, 10); @@ -192,7 +192,8 @@ namespace osu.Game.Screens.Menu shader = Source.shader; texture = Source.texture; size = Source.DrawSize.X; - audioData = Source.frequencyAmplitudes; + + Source.frequencyAmplitudes.AsSpan().CopyTo(audioData); } public override void Draw(Action vertexAction) From f400249f329627c71a5d2786d7629ee7f95596d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Mar 2022 18:28:38 +0100 Subject: [PATCH 130/285] Fix multiplayer match settings overlay dropdown Z-ordering --- .../OnlinePlay/Match/Components/RoomSettingsOverlay.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs index 435c9aca02..c15b5b443a 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; @@ -93,7 +94,12 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components { } - protected class SectionContainer : FillFlowContainer
+ /// + /// is used to ensure that if the nested s + /// use expanded overhanging content (like an 's dropdown), + /// then the overhanging content will be correctly Z-ordered. + /// + protected class SectionContainer : ReverseChildIDFillFlowContainer
{ public SectionContainer() { From 69cc8636118a20400e5979b0dff958ea88176a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Mar 2022 19:28:07 +0100 Subject: [PATCH 131/285] Fix test failures in skin editor test scene --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 38d83058c0..74ce9726e7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -37,6 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay Player.ScaleTo(0.4f); LoadComponentAsync(skinEditor = new SkinEditor(Player), Add); }); + AddUntilStep("wait for loaded", () => skinEditor.IsLoaded); } [Test] From 8c5594b3eadd8afb6db153d0726eaa0b48359f02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 27 Mar 2022 14:38:06 +0900 Subject: [PATCH 132/285] Update fastlane (fix authorization failure) --- Gemfile.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1010027af9..ddab497657 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,17 +8,17 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.553.0) - aws-sdk-core (3.126.0) + aws-partitions (1.570.0) + aws-sdk-core (3.130.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.54.0) - aws-sdk-core (~> 3, >= 3.126.0) + aws-sdk-kms (1.55.0) + aws-sdk-core (~> 3, >= 3.127.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.112.0) - aws-sdk-core (~> 3, >= 3.126.0) + aws-sdk-s3 (1.113.0) + aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.4.0) @@ -36,8 +36,8 @@ GEM unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) emoji_regex (3.2.3) - excon (0.91.0) - faraday (1.9.3) + excon (0.92.1) + faraday (1.10.0) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -66,7 +66,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.204.2) + fastlane (2.205.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -130,10 +130,10 @@ GEM google-cloud-core (1.6.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) - google-cloud-env (1.5.0) - faraday (>= 0.17.3, < 2.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) google-cloud-errors (1.2.0) - google-cloud-storage (1.36.0) + google-cloud-storage (1.36.1) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) @@ -141,8 +141,8 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.1.0) - faraday (>= 0.17.3, < 2.0) + googleauth (1.1.2) + faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) memoist (~> 0.16) multi_json (~> 1.11) @@ -152,7 +152,7 @@ GEM http-cookie (1.0.4) domain_name (~> 0.5) httpclient (2.8.3) - jmespath (1.5.0) + jmespath (1.6.1) json (2.6.1) jwt (2.3.0) memoist (0.16.2) @@ -182,9 +182,9 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) - signet (0.16.0) + signet (0.16.1) addressable (~> 2.8) - faraday (>= 0.17.3, < 2.0) + faraday (>= 0.17.5, < 3.0) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) simctl (1.6.8) @@ -205,7 +205,7 @@ GEM uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.8) + unf_ext (0.0.8.1) unicode-display_width (1.8.0) webrick (1.7.0) word_wrap (1.0.0) From de1fbda648a1ce13522b5f0a8faa602161df5e58 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 27 Mar 2022 20:54:56 +0300 Subject: [PATCH 133/285] Clarify that searching includes both issues and Q&A discussions --- .github/ISSUE_TEMPLATE/bug-issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml index 5b19c3732c..d77b28316a 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.yml +++ b/.github/ISSUE_TEMPLATE/bug-issue.yml @@ -9,7 +9,7 @@ body: Important to note that your issue may have already been reported before. Please check: - Pinned issues, at the top of https://github.com/ppy/osu/issues. - Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0). - - And most importantly, search for your issue. If you find that it already exists, respond with a reaction or add any further information that may be helpful. + - And most importantly, search across the [issue listing](https://github.com/ppy/osu/issues) and [Q&A discussion listing](https://github.com/ppy/osu/discussions/categories/q-a) for your issue. If you find that it already exists, respond with a reaction or add any further information that may be helpful. - type: dropdown attributes: From f847f9a31592d98e5070eaad37ef8e48cf313ae9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 27 Mar 2022 20:57:00 +0300 Subject: [PATCH 134/285] Exclude "open osu! folder" logs procedure from mobile platforms --- .github/ISSUE_TEMPLATE/bug-issue.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml index d77b28316a..8f54b5d1c8 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.yml +++ b/.github/ISSUE_TEMPLATE/bug-issue.yml @@ -48,19 +48,27 @@ body: Attaching log files is required for every reported bug. See instructions below on how to find them. + **Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead. + + ### Desktop platforms + If the game has not yet been closed since you found the bug: 1. Head on to game settings and click on "Open osu! folder" 2. Then open the `logs` folder located there - **Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead. - - The default places to find the logs are as follows: + The default places to find the logs on desktop platforms are as follows: - `%AppData%/osu/logs` *on Windows* - `~/.local/share/osu/logs` *on Linux & macOS* + + If you have selected a custom location for the game files, you can find the `logs` folder there. + + ### Mobile platforms + + The places to find the logs on mobile platforms are as follows: - `Android/data/sh.ppy.osulazer/files/logs` *on Android* - *On iOS*, they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer) - If you have selected a custom location for the game files, you can find the `logs` folder there. + --- After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below. From ebf520921520d486fbf99cd8c07b0e611b81fc0b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 27 Mar 2022 21:38:55 +0300 Subject: [PATCH 135/285] Reword issue searching note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .github/ISSUE_TEMPLATE/bug-issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml index 8f54b5d1c8..ea5ee298fb 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.yml +++ b/.github/ISSUE_TEMPLATE/bug-issue.yml @@ -9,7 +9,7 @@ body: Important to note that your issue may have already been reported before. Please check: - Pinned issues, at the top of https://github.com/ppy/osu/issues. - Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0). - - And most importantly, search across the [issue listing](https://github.com/ppy/osu/issues) and [Q&A discussion listing](https://github.com/ppy/osu/discussions/categories/q-a) for your issue. If you find that it already exists, respond with a reaction or add any further information that may be helpful. + - And most importantly, search for your issue both in the [issue listing](https://github.com/ppy/osu/issues) and the [Q&A discussion listing](https://github.com/ppy/osu/discussions/categories/q-a). If you find that it already exists, respond with a reaction or add any further information that may be helpful. - type: dropdown attributes: From a98aac3bf2bbd0e03c46f4fb5eea7182e7b53d47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Mar 2022 17:33:15 +0900 Subject: [PATCH 136/285] Better inform users of migration failure reason when running Apply Silicon build As mentioned in https://github.com/ppy/osu/discussions/17409#discussioncomment-2445464? --- osu.Game/Database/EFToRealmMigrator.cs | 34 ++++++++++++++++++++------ 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index cbf5c5ffe9..d3f7c561d5 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -1,6 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + +using System; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -14,6 +17,7 @@ using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Models; @@ -29,8 +33,6 @@ using SharpCompress.Archives.Zip; using SharpCompress.Common; using SharpCompress.Writers.Zip; -#nullable enable - namespace osu.Game.Database { internal class EFToRealmMigrator : CompositeDrawable @@ -57,7 +59,7 @@ namespace osu.Game.Database [Resolved] private Storage storage { get; set; } = null!; - private readonly OsuSpriteText currentOperationText; + private readonly OsuTextFlowContainer currentOperationText; public EFToRealmMigrator() { @@ -99,11 +101,13 @@ namespace osu.Game.Database { State = { Value = Visibility.Visible } }, - currentOperationText = new OsuSpriteText + currentOperationText = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 30)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.Default.With(size: 30) + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + TextAnchor = Anchor.TopCentre, }, } }, @@ -147,19 +151,33 @@ namespace osu.Game.Database log("Migration successful!"); if (DebugUtils.IsDebugBuild) - Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important); + { + Logger.Log( + "Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", + level: LogLevel.Important); + } } else { log("Migration failed!"); Logger.Log(t.Exception.ToString(), LoggingTarget.Database); + if (t.Exception.Flatten().InnerException is TypeInitializationException) + { + // Not guaranteed to be the only cause of exception, but let's roll with it for now. + log("Please download and run the intel version of osu! once\nto allow data migration to complete!"); + return; + } + notificationOverlay.Post(new SimpleErrorNotification { - Text = "IMPORTANT: During data migration, some of your data could not be successfully migrated. The previous version has been backed up.\n\nFor further assistance, please open a discussion on github and attach your backup files (click to get started).", + Text = + "IMPORTANT: During data migration, some of your data could not be successfully migrated. The previous version has been backed up.\n\nFor further assistance, please open a discussion on github and attach your backup files (click to get started).", Activated = () => { - game.OpenUrlExternally($@"https://github.com/ppy/osu/discussions/new?title=Realm%20migration%20issue ({t.Exception.Message})&body=Please%20drag%20the%20""attach_me.zip""%20file%20here!&category=q-a", true); + game.OpenUrlExternally( + $@"https://github.com/ppy/osu/discussions/new?title=Realm%20migration%20issue ({t.Exception.Message})&body=Please%20drag%20the%20""attach_me.zip""%20file%20here!&category=q-a", + true); const string attachment_filename = "attach_me.zip"; const string backup_folder = "backups"; From b5834dabdb12a9c63d4b676fb906ee01896a372a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Mar 2022 20:43:23 +0900 Subject: [PATCH 137/285] Fix skin editor not exiting correctly when using the menu Closes https://github.com/ppy/osu/issues/17489. --- osu.Game/Skinning/Editor/SkinEditor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 4cc7e0bcdb..7bf4e94662 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -42,6 +42,9 @@ namespace osu.Game.Skinning.Editor [Resolved] private OsuColour colours { get; set; } + [Resolved(canBeNull: true)] + private SkinEditorOverlay skinEditorOverlay { get; set; } + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); @@ -107,7 +110,7 @@ namespace osu.Game.Skinning.Editor new EditorMenuItem("Save", MenuItemType.Standard, Save), new EditorMenuItem("Revert to default", MenuItemType.Destructive, revert), new EditorMenuItemSpacer(), - new EditorMenuItem("Exit", MenuItemType.Standard, Hide), + new EditorMenuItem("Exit", MenuItemType.Standard, () => skinEditorOverlay?.Hide()), }, }, } From a0692ce47780c05a907439172f3ae85a6b0b80a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Mar 2022 22:20:56 +0900 Subject: [PATCH 138/285] Add a `const` for system users which should never display a profile --- osu.Game/Online/API/Requests/Responses/APIUser.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index a53ac1cd9b..a87f0811a1 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -15,6 +15,11 @@ namespace osu.Game.Online.API.Requests.Responses [JsonObject(MemberSerialization.OptIn)] public class APIUser : IEquatable, IUser { + /// + /// A user ID which can be used to represent any system user which is not attached to a user profile. + /// + public const int SYSTEM_USER_ID = 0; + [JsonProperty(@"id")] public int Id { get; set; } = 1; @@ -238,7 +243,7 @@ namespace osu.Game.Online.API.Requests.Responses ///
public static readonly APIUser SYSTEM_USER = new APIUser { - Id = 0, + Id = SYSTEM_USER_ID, Username = "system", Colour = @"9c0101", }; From d2d88015e32927ab8704b273c2a090630cce084c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Mar 2022 22:21:23 +0900 Subject: [PATCH 139/285] Update all cinema/autoplay mods to specify the system user ID --- .../Mods/EmptyFreeformModAutoplay.cs | 6 +++++- .../Mods/PippidonModAutoplay.cs | 6 +++++- .../Mods/EmptyScrollingModAutoplay.cs | 6 +++++- .../Mods/PippidonModAutoplay.cs | 6 +++++- osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs | 9 ++++++++- osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs | 9 ++++++++- osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs | 9 ++++++++- osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs | 9 ++++++++- osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs | 9 ++++++++- osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs | 9 ++++++++- osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs | 9 ++++++++- osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs | 9 ++++++++- 12 files changed, 84 insertions(+), 12 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs index d4496a24fd..9c2a108c4e 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs @@ -16,7 +16,11 @@ namespace osu.Game.Rulesets.EmptyFreeform.Mods { ScoreInfo = new ScoreInfo { - User = new APIUser { Username = "sample" }, + User = new APIUser + { + Id = APIUser.SYSTEM_USER_ID, + Username = "sample" + }, }, Replay = new EmptyFreeformAutoGenerator(beatmap).Generate(), }; diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs index 6e1fe42ee2..6683856a1a 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs @@ -16,7 +16,11 @@ namespace osu.Game.Rulesets.Pippidon.Mods { ScoreInfo = new ScoreInfo { - User = new APIUser { Username = "sample" }, + User = new APIUser + { + Id = APIUser.SYSTEM_USER_ID, + Username = "sample" + }, }, Replay = new PippidonAutoGenerator(beatmap).Generate(), }; diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs index c5bacb522f..e3cecc7b44 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs @@ -16,7 +16,11 @@ namespace osu.Game.Rulesets.EmptyScrolling.Mods { ScoreInfo = new ScoreInfo { - User = new APIUser { Username = "sample" }, + User = new APIUser + { + Id = APIUser.SYSTEM_USER_ID, + Username = "sample" + }, }, Replay = new EmptyScrollingAutoGenerator(beatmap).Generate(), }; diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs index 6e1fe42ee2..6683856a1a 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs @@ -16,7 +16,11 @@ namespace osu.Game.Rulesets.Pippidon.Mods { ScoreInfo = new ScoreInfo { - User = new APIUser { Username = "sample" }, + User = new APIUser + { + Id = APIUser.SYSTEM_USER_ID, + Username = "sample" + }, }, Replay = new PippidonAutoGenerator(beatmap).Generate(), }; diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs index 11fffb31de..fbbb610035 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs @@ -14,7 +14,14 @@ namespace osu.Game.Rulesets.Catch.Mods { public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { - ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!salad" } }, + ScoreInfo = new ScoreInfo + { + User = new APIUser + { + Id = APIUser.SYSTEM_USER_ID, + Username = "osu!salad" + } + }, Replay = new CatchAutoGenerator(beatmap).Generate(), }; } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs index 6d2286b957..2f4769048f 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs @@ -15,7 +15,14 @@ namespace osu.Game.Rulesets.Catch.Mods { public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { - ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!salad" } }, + ScoreInfo = new ScoreInfo + { + User = new APIUser + { + Id = APIUser.SYSTEM_USER_ID, + Username = "osu!salad" + } + }, Replay = new CatchAutoGenerator(beatmap).Generate(), }; } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs index 1504c868d0..4eea0d4a44 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs @@ -15,7 +15,14 @@ namespace osu.Game.Rulesets.Mania.Mods { public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { - ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!topus" } }, + ScoreInfo = new ScoreInfo + { + User = new APIUser + { + Id = APIUser.SYSTEM_USER_ID, + Username = "osu!topus" + } + }, Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), }; } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs index 4f1276946b..0aa419bbcb 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs @@ -16,7 +16,14 @@ namespace osu.Game.Rulesets.Mania.Mods { public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { - ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!topus" } }, + ScoreInfo = new ScoreInfo + { + User = new APIUser + { + Id = APIUser.SYSTEM_USER_ID, + Username = "osu!topus" + } + }, Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), }; } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs index 2668013321..c5b1c55c40 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs @@ -18,7 +18,14 @@ namespace osu.Game.Rulesets.Osu.Mods public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { - ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } }, + ScoreInfo = new ScoreInfo + { + User = new APIUser + { + Id = APIUser.SYSTEM_USER_ID, + Username = "Autoplay", + } + }, Replay = new OsuAutoGenerator(beatmap, mods).Generate() }; } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs index ff31cfcd18..83e6f0efb5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs @@ -19,7 +19,14 @@ namespace osu.Game.Rulesets.Osu.Mods public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { - ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } }, + ScoreInfo = new ScoreInfo + { + User = new APIUser + { + Id = APIUser.SYSTEM_USER_ID, + Username = "Autoplay" + } + }, Replay = new OsuAutoGenerator(beatmap, mods).Generate() }; } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs index 5832ae3dc1..b9bf3ab1b4 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs @@ -14,7 +14,14 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { - ScoreInfo = new ScoreInfo { User = new APIUser { Username = "mekkadosu!" } }, + ScoreInfo = new ScoreInfo + { + User = new APIUser + { + Id = APIUser.SYSTEM_USER_ID, + Username = "mekkadosu!" + } + }, Replay = new TaikoAutoGenerator(beatmap).Generate(), }; } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs index f76e04a069..6ecf8a3b3d 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs @@ -15,7 +15,14 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { - ScoreInfo = new ScoreInfo { User = new APIUser { Username = "mekkadosu!" } }, + ScoreInfo = new ScoreInfo + { + User = new APIUser + { + Id = APIUser.SYSTEM_USER_ID, + Username = "mekkadosu!" + } + }, Replay = new TaikoAutoGenerator(beatmap).Generate(), }; } From dc7dc7f65ac7a3ef3a0f3d1cd9edd06ae0186099 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Mar 2022 22:50:31 +0900 Subject: [PATCH 140/285] Update `UserProfileOverlay` and `ClickableAvatar` to treat system users more correctly --- osu.Game/Overlays/UserProfileOverlay.cs | 2 +- osu.Game/Users/Drawables/ClickableAvatar.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 9fac1463f2..518a2bf9c8 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays public void ShowUser(IUser user) { - if (user == APIUser.SYSTEM_USER) + if (user.OnlineID == APIUser.SYSTEM_USER_ID) return; Show(); diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index 34c87568a1..0dd135b500 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -21,7 +21,7 @@ namespace osu.Game.Users.Drawables /// public bool OpenOnClick { - set => clickableArea.Enabled.Value = value; + set => clickableArea.Enabled.Value = clickableArea.Action != null && value; } /// @@ -52,8 +52,10 @@ namespace osu.Game.Users.Drawables Add(clickableArea = new ClickableArea { RelativeSizeAxes = Axes.Both, - Action = openProfile }); + + if (user?.Id != APIUser.SYSTEM_USER_ID) + clickableArea.Action = openProfile; } [BackgroundDependencyLoader] From 05a978ce8c255d0ebfaf395f68f31d3446e3f281 Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Mon, 28 Mar 2022 16:36:37 +0200 Subject: [PATCH 141/285] Update LegacyComboCounter layout and scaling --- .../Screens/Play/HUD/LegacyComboCounter.cs | 58 +++++++++++++------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 9510453ba5..a49d09e839 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -21,7 +21,9 @@ namespace osu.Game.Screens.Play.HUD private uint scheduledPopOutCurrentId; - private const double pop_out_duration = 150; + private const double big_pop_out_duration = 300; + + private const double small_pop_out_duration = 100; private const double fade_out_duration = 100; @@ -65,32 +67,28 @@ namespace osu.Game.Screens.Play.HUD Margin = new MarginPadding(10); - Scale = new Vector2(1.2f); + Scale = new Vector2(1.3f); InternalChildren = new[] { counterContainer = new Container { - AutoSizeAxes = Axes.Both, AlwaysPresent = true, Children = new[] { popOutCount = new LegacySpriteText(LegacyFont.Combo) { Alpha = 0, - Margin = new MarginPadding(0.05f), Blending = BlendingParameters.Additive, Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, BypassAutoSizeAxes = Axes.Both, }, displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo) { - // Initial text and AlwaysPresent allow the counter to have a size before it first displays a combo. - // This is useful for display in the skin editor. - Text = formatCount(0), - AlwaysPresent = true, Alpha = 0, + AlwaysPresent = true, + Anchor = Anchor.BottomLeft, + BypassAutoSizeAxes = Axes.Both, }, } } @@ -120,9 +118,12 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load(ScoreProcessor scoreProcessor) + private void load(ScoreProcessor scoreProcessor, ISkinSource skin) { Current.BindTo(scoreProcessor.Combo); + + // Since layout depends on combo font height we need to update it during skin change + skin.SourceChanged += updateLayout; } protected override void LoadComplete() @@ -130,10 +131,28 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); ((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value); + ((IHasText)popOutCount).Text = formatCount(Current.Value); + + updateLayout(); Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true); } + private void updateLayout() + { + // Eye-balled to match stable + const float font_height_ratio = 0.625f; + const float vertical_offset = 9; + + displayedCountSpriteText.OriginPosition = new Vector2(0, font_height_ratio * displayedCountSpriteText.Height + vertical_offset); + displayedCountSpriteText.Position = new Vector2(0, -(1 - font_height_ratio) * displayedCountSpriteText.Height + vertical_offset); + + popOutCount.OriginPosition = new Vector2(3, font_height_ratio * popOutCount.Height + vertical_offset); // In stable, the bigger pop out scales a bit to the left + popOutCount.Position = new Vector2(0, -(1 - font_height_ratio) * popOutCount.Height + vertical_offset); + + counterContainer.Size = displayedCountSpriteText.Size; + } + private void updateCount(bool rolling) { int prev = previousValue; @@ -165,26 +184,29 @@ namespace osu.Game.Screens.Play.HUD ((IHasText)popOutCount).Text = formatCount(newValue); popOutCount.ScaleTo(1.6f); - popOutCount.FadeTo(0.75f); - popOutCount.MoveTo(Vector2.Zero); + popOutCount.FadeTo(0.6f); - popOutCount.ScaleTo(1, pop_out_duration); - popOutCount.FadeOut(pop_out_duration); - popOutCount.MoveTo(displayedCountSpriteText.Position, pop_out_duration); + popOutCount.ScaleTo(1, big_pop_out_duration); + popOutCount.FadeOut(big_pop_out_duration); } private void transformNoPopOut(int newValue) { ((IHasText)displayedCountSpriteText).Text = formatCount(newValue); + counterContainer.Size = displayedCountSpriteText.Size; + displayedCountSpriteText.ScaleTo(1); } private void transformPopOutSmall(int newValue) { ((IHasText)displayedCountSpriteText).Text = formatCount(newValue); - displayedCountSpriteText.ScaleTo(1.1f); - displayedCountSpriteText.ScaleTo(1, pop_out_duration); + + counterContainer.Size = displayedCountSpriteText.Size; + + displayedCountSpriteText.ScaleTo(1.1f, small_pop_out_duration / 2, Easing.InQuad).Then() + .ScaleTo(1, small_pop_out_duration / 2, Easing.OutQuad); } private void scheduledPopOutSmall(uint id) @@ -212,7 +234,7 @@ namespace osu.Game.Screens.Play.HUD Scheduler.AddDelayed(delegate { scheduledPopOutSmall(newTaskId); - }, pop_out_duration); + }, big_pop_out_duration - 140); } private void onCountRolling(int currentValue, int newValue) From 7059e4bf9463a1adf45b2dc707e9cba5f4ac2be7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Mar 2022 23:37:39 +0900 Subject: [PATCH 142/285] Add test coverage for autoplay avatar not being clickable --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index fdc3916c47..518ab5a1d3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -5,12 +5,14 @@ using System.ComponentModel; using System.Linq; using osu.Framework.Testing; using osu.Game.Beatmaps.Timing; +using osu.Game.Graphics.Containers; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.Break; using osu.Game.Screens.Ranking; +using osu.Game.Users.Drawables; namespace osu.Game.Tests.Visual.Gameplay { @@ -39,11 +41,18 @@ namespace osu.Game.Tests.Visual.Gameplay seekToBreak(1); AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); - AddUntilStep("results displayed", () => getResultsScreen() != null); + + AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true); AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100); AddAssert("score has no misses", () => getResultsScreen().Score.Statistics[HitResult.Miss] == 0); + AddUntilStep("avatar dispalyed", () => getAvatar() != null); + AddAssert("avatar not clickable", () => getAvatar().ChildrenOfType().First().Action == null); + + ClickableAvatar getAvatar() => getResultsScreen() + .ChildrenOfType().FirstOrDefault(); + ResultsScreen getResultsScreen() => Stack.CurrentScreen as ResultsScreen; } From b6ae0ebb6f9ec7b561659d67327971e1a484ff87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Mar 2022 23:46:04 +0900 Subject: [PATCH 143/285] Limit macOS specific log output to macOS platforms specifically --- osu.Game/Database/EFToRealmMigrator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index d3f7c561d5..d6460bc9d3 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Graphics; @@ -162,7 +163,7 @@ namespace osu.Game.Database log("Migration failed!"); Logger.Log(t.Exception.ToString(), LoggingTarget.Database); - if (t.Exception.Flatten().InnerException is TypeInitializationException) + if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && t.Exception.Flatten().InnerException is TypeInitializationException) { // Not guaranteed to be the only cause of exception, but let's roll with it for now. log("Please download and run the intel version of osu! once\nto allow data migration to complete!"); From ea46e024c1f883fb73d655010fe69853e9bfa22e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Mar 2022 23:59:11 +0900 Subject: [PATCH 144/285] Avoid deadlock during migration failed exit process --- osu.Game/Database/EFToRealmMigrator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index d6460bc9d3..ae73e13b77 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -167,6 +167,7 @@ namespace osu.Game.Database { // Not guaranteed to be the only cause of exception, but let's roll with it for now. log("Please download and run the intel version of osu! once\nto allow data migration to complete!"); + efContextFactory.SetMigrationCompletion(); return; } From 001a98e069940a69eb4d30d1e036eb42f95ac62e Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Mon, 28 Mar 2022 18:08:35 +0200 Subject: [PATCH 145/285] Fix LegacyComboCounter not unsubscribing from skin --- .../Screens/Play/HUD/LegacyComboCounter.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index a49d09e839..158cb92a1c 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -44,6 +44,9 @@ namespace osu.Game.Screens.Play.HUD private readonly Container counterContainer; + [Resolved] + private ISkinSource skin { get; set; } + /// /// Hides the combo counter internally without affecting its . /// @@ -118,12 +121,9 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load(ScoreProcessor scoreProcessor, ISkinSource skin) + private void load(ScoreProcessor scoreProcessor) { Current.BindTo(scoreProcessor.Combo); - - // Since layout depends on combo font height we need to update it during skin change - skin.SourceChanged += updateLayout; } protected override void LoadComplete() @@ -133,9 +133,11 @@ namespace osu.Game.Screens.Play.HUD ((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value); ((IHasText)popOutCount).Text = formatCount(Current.Value); - updateLayout(); - Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true); + + // Since layout depends on combo font height we need to update it during skin change + skin.SourceChanged += updateLayout; + updateLayout(); } private void updateLayout() @@ -290,5 +292,13 @@ namespace osu.Game.Screens.Play.HUD double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue; return difference * rolling_duration; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (skin != null) + skin.SourceChanged -= updateLayout; + } } } From 265b2111ef176f9178e49005dd0e0b3c6404a475 Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Mon, 28 Mar 2022 18:08:51 +0200 Subject: [PATCH 146/285] Remove comment --- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 158cb92a1c..b4c972b94e 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -142,7 +142,6 @@ namespace osu.Game.Screens.Play.HUD private void updateLayout() { - // Eye-balled to match stable const float font_height_ratio = 0.625f; const float vertical_offset = 9; From e3289bb0804bd3977c0192592c4a1fb5a54198d2 Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Mon, 28 Mar 2022 19:33:00 +0200 Subject: [PATCH 147/285] Don't scale big pop out as much --- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index b4c972b94e..f463ac1457 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Play.HUD Margin = new MarginPadding(10); - Scale = new Vector2(1.3f); + Scale = new Vector2(1.28f); InternalChildren = new[] { @@ -184,7 +184,7 @@ namespace osu.Game.Screens.Play.HUD { ((IHasText)popOutCount).Text = formatCount(newValue); - popOutCount.ScaleTo(1.6f); + popOutCount.ScaleTo(1.56f); popOutCount.FadeTo(0.6f); popOutCount.ScaleTo(1, big_pop_out_duration); From 981ef735cfd87b1873fd476cfc16c24e53ec746e Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Mon, 28 Mar 2022 19:57:59 +0200 Subject: [PATCH 148/285] Always start small pop out from its initial size --- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index f463ac1457..58036320d0 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -206,7 +206,11 @@ namespace osu.Game.Screens.Play.HUD counterContainer.Size = displayedCountSpriteText.Size; - displayedCountSpriteText.ScaleTo(1.1f, small_pop_out_duration / 2, Easing.InQuad).Then() + // In stable, small pop out always starts from its initial size + displayedCountSpriteText.ClearTransforms(); + + displayedCountSpriteText.ScaleTo(1).Then() + .ScaleTo(1.1f, small_pop_out_duration / 2, Easing.InQuad).Then() .ScaleTo(1, small_pop_out_duration / 2, Easing.OutQuad); } From caf641e1ab19c9880db45e82e9f8c63b72c32d91 Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Mon, 28 Mar 2022 23:50:35 +0200 Subject: [PATCH 149/285] Remove redundant ClearTransforms --- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 58036320d0..1617d49b10 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -206,9 +206,6 @@ namespace osu.Game.Screens.Play.HUD counterContainer.Size = displayedCountSpriteText.Size; - // In stable, small pop out always starts from its initial size - displayedCountSpriteText.ClearTransforms(); - displayedCountSpriteText.ScaleTo(1).Then() .ScaleTo(1.1f, small_pop_out_duration / 2, Easing.InQuad).Then() .ScaleTo(1, small_pop_out_duration / 2, Easing.OutQuad); From 375a752e24faee7a9ae21b004e2c69e651b19f15 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 29 Mar 2022 11:24:26 +0900 Subject: [PATCH 150/285] Fix countdown timer not refreshing on new countdown --- .../Multiplayer/Match/MultiplayerReadyButton.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 0ec2c6560a..62be9ad3bd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -36,17 +36,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } private MultiplayerCountdown countdown; - private DateTimeOffset countdownReceivedTime; + private DateTimeOffset countdownChangeTime; private ScheduledDelegate countdownUpdateDelegate; private void onRoomUpdated() => Scheduler.AddOnce(() => { - if (countdown == null && room?.Countdown != null) - countdownReceivedTime = DateTimeOffset.Now; + if (countdown != room?.Countdown) + { + countdown = room?.Countdown; + countdownChangeTime = DateTimeOffset.Now; + } - countdown = room?.Countdown; - - if (room?.Countdown != null) + if (countdown != null) countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 100, true); else { @@ -74,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (countdown != null) { - TimeSpan timeElapsed = DateTimeOffset.Now - countdownReceivedTime; + TimeSpan timeElapsed = DateTimeOffset.Now - countdownChangeTime; TimeSpan countdownRemaining; if (timeElapsed > countdown.TimeRemaining) From 0433d2fe6a120f3b28140d24ae0026c34d7a4e91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Mar 2022 11:40:58 +0900 Subject: [PATCH 151/285] Add safety to realm instance retrieval in `RealmAccess` --- osu.Game/Database/RealmAccess.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 8574002436..8dbb338980 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -13,6 +13,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Input.Bindings; @@ -293,7 +294,18 @@ namespace osu.Game.Database /// Compact this realm. /// /// - public bool Compact() => Realm.Compact(getConfiguration()); + public bool Compact() + { + try + { + return Realm.Compact(getConfiguration()); + } + // Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator). + catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException) + { + return true; + } + } /// /// Run work on realm with a return value. @@ -542,6 +554,11 @@ namespace osu.Game.Database return Realm.GetInstance(getConfiguration()); } + // Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator). + catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException) + { + return Realm.GetInstance(); + } finally { if (tookSemaphoreLock) From 8948bcce19e1e1112cc64b2541fe6bb21444793c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Mar 2022 13:18:31 +0900 Subject: [PATCH 152/285] Restructure transforms slightly --- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 1617d49b10..afcdf5d07f 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -184,11 +184,11 @@ namespace osu.Game.Screens.Play.HUD { ((IHasText)popOutCount).Text = formatCount(newValue); - popOutCount.ScaleTo(1.56f); - popOutCount.FadeTo(0.6f); + popOutCount.ScaleTo(1.56f) + .ScaleTo(1, big_pop_out_duration); - popOutCount.ScaleTo(1, big_pop_out_duration); - popOutCount.FadeOut(big_pop_out_duration); + popOutCount.FadeTo(0.6f) + .FadeOut(big_pop_out_duration); } private void transformNoPopOut(int newValue) From 327477d050910ee092ca6259633be172d525c086 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Mar 2022 16:49:52 +0900 Subject: [PATCH 153/285] Remove unnecessary `SetReplayScore` call in `ModCinema` --- osu.Game/Rulesets/Mods/ModCinema.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index f28ef1edeb..1fe5ddecf9 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -14,8 +14,6 @@ namespace osu.Game.Rulesets.Mods { public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods)); - // AlwaysPresent required for hitsounds drawableRuleset.AlwaysPresent = true; drawableRuleset.Hide(); From 3fc8c23fe440644f321c9fa41ca183cf0b0ae637 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Mar 2022 16:49:52 +0900 Subject: [PATCH 154/285] Remove unnecessary `SetReplayScore` call in `ModCinema` --- osu.Game/Rulesets/Mods/ModCinema.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index f28ef1edeb..1fe5ddecf9 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -14,8 +14,6 @@ namespace osu.Game.Rulesets.Mods { public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods)); - // AlwaysPresent required for hitsounds drawableRuleset.AlwaysPresent = true; drawableRuleset.Hide(); From 7d716adf39a8f91d1bac35332ad3346fa3020d67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Mar 2022 16:27:42 +0900 Subject: [PATCH 155/285] Create new `ICreateReplayData` interface and obsolete `ICreateReplay` --- osu.Game/Rulesets/Mods/ICreateReplay.cs | 10 +++- osu.Game/Rulesets/Mods/ICreateReplayData.cs | 58 +++++++++++++++++++++ osu.Game/Rulesets/Mods/ModAutoplay.cs | 15 +++++- 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/ICreateReplayData.cs diff --git a/osu.Game/Rulesets/Mods/ICreateReplay.cs b/osu.Game/Rulesets/Mods/ICreateReplay.cs index 098bd8799a..4883ce5842 100644 --- a/osu.Game/Rulesets/Mods/ICreateReplay.cs +++ b/osu.Game/Rulesets/Mods/ICreateReplay.cs @@ -1,14 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Scoring; namespace osu.Game.Rulesets.Mods { - public interface ICreateReplay + [Obsolete("Use ICreateReplayData instead")] // Can be removed 20220929 + public interface ICreateReplay : ICreateReplayData { public Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods); + + ModReplayData ICreateReplayData.CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + { + var replayScore = CreateReplayScore(beatmap, mods); + return new ModReplayData(replayScore.Replay, new ModCreatedReplayUser { Username = replayScore.ScoreInfo.User.Username }); + } } } diff --git a/osu.Game/Rulesets/Mods/ICreateReplayData.cs b/osu.Game/Rulesets/Mods/ICreateReplayData.cs new file mode 100644 index 0000000000..b022949345 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ICreateReplayData.cs @@ -0,0 +1,58 @@ +// 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.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Replays; +using osu.Game.Users; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// A mod which creates full replay data, which is to be played back in place of a local user playing the game. + /// + public interface ICreateReplayData + { + /// + /// Create replay data. + /// + /// + /// + /// + public ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods); + } + + /// + /// Data created by a mod that implements . + /// + public class ModReplayData + { + /// + /// The full replay data. + /// + public readonly Replay Replay; + + /// + /// Placeholder user data to show in place of the local user when the associated mod is active. + /// + public readonly ModCreatedReplayUser User; + + public ModReplayData(Replay replay, ModCreatedReplayUser user = null) + { + Replay = replay; + User = user ?? new ModCreatedReplayUser(); + } + } + + /// + /// A user which is associated with a replay that was created by a mod (ie. autoplay or cinema). + /// + public class ModCreatedReplayUser : IUser + { + public int OnlineID => APIUser.SYSTEM_USER_ID; + public bool IsBot => true; + + public string Username { get; set; } = string.Empty; + } +} diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 60b9c29fe0..aa72812bc3 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics.Sprites; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Replays; @@ -11,7 +12,7 @@ using osu.Game.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplay + public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplayData { public override string Name => "Autoplay"; public override string Acronym => "AT"; @@ -30,6 +31,18 @@ namespace osu.Game.Rulesets.Mods public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; + [Obsolete("Use CreateScoreFromReplayData(IBeatmap, IReadOnlyList) instead")] // Can be removed 20220929 public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { Replay = new Replay() }; + + public virtual ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + { + Logger.Log($"Ruleset mod implementation for {GetType().Name} should be updated to newer {nameof(ICreateReplayData)} signature.", LoggingTarget.Information); + +#pragma warning disable CS0618 + var replayScore = CreateReplayScore(beatmap, mods); +#pragma warning restore CS0618 + + return new ModReplayData(replayScore.Replay, new ModCreatedReplayUser { Username = replayScore.ScoreInfo.User.Username }); + } } } From ea9495eb74e42393063675dca467f5edd426e39c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Mar 2022 16:45:21 +0900 Subject: [PATCH 156/285] Update all existing calls to extension method with correct fallback handling --- .../Visual/Gameplay/TestSceneReplay.cs | 2 +- .../Edit/DrawableEditorRulesetWrapper.cs | 2 +- osu.Game/Rulesets/Mods/ModExtensions.cs | 31 +++++++++++++++++++ osu.Game/Screens/Select/PlaySongSelect.cs | 24 ++++++++++++-- .../Skinning/Editor/SkinEditorSceneLibrary.cs | 3 +- osu.Game/Tests/Visual/TestPlayer.cs | 2 +- osu.Game/Tests/Visual/TestReplayPlayer.cs | 2 +- 7 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/ModExtensions.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs index f94e122b30..8622fe8f53 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty()); - return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod()?.CreateReplayScore(beatmap, Array.Empty())); + return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod()?.CreateScoreFromReplayData(beatmap, Array.Empty())); } protected override void AddCheckSteps() diff --git a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs index 071f01ca00..392a5db9da 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditorRulesetWrapper.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Edit private void regenerateAutoplay() { var autoplayMod = drawableRuleset.Mods.OfType().Single(); - drawableRuleset.SetReplayScore(autoplayMod.CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods)); + drawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(drawableRuleset.Beatmap, drawableRuleset.Mods)); } private void addHitObject(HitObject hitObject) diff --git a/osu.Game/Rulesets/Mods/ModExtensions.cs b/osu.Game/Rulesets/Mods/ModExtensions.cs new file mode 100644 index 0000000000..b22030414b --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModExtensions.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 System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Scoring; + +namespace osu.Game.Rulesets.Mods +{ + public static class ModExtensions + { + public static Score CreateScoreFromReplayData(this ICreateReplayData mod, IBeatmap beatmap, IReadOnlyList mods) + { + var replayData = mod.CreateReplayData(beatmap, mods); + + return new Score + { + Replay = replayData.Replay, + ScoreInfo = + { + User = new APIUser + { + Id = APIUser.SYSTEM_USER_ID, + Username = replayData.User.Username, + } + } + }; + } + } +} diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 94aa165785..17dc4524fe 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Game.Graphics; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Mods; @@ -111,9 +112,28 @@ namespace osu.Game.Screens.Select Player createPlayer() { - var replayGeneratingMod = Mods.Value.OfType().FirstOrDefault(); + var replayGeneratingMod = Mods.Value.OfType().FirstOrDefault(); + if (replayGeneratingMod != null) - return new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateReplayScore(beatmap, mods)); + { + return new ReplayPlayer((beatmap, mods) => + { + var replayData = replayGeneratingMod.CreateReplayData(beatmap, mods); + + return new Score + { + Replay = replayData.Replay, + ScoreInfo = + { + User = new APIUser + { + Id = APIUser.SYSTEM_USER_ID, + Username = replayData.User.Username, + } + } + }; + }); + } return new SoloPlayer(); } diff --git a/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs b/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs index d126eff075..0808cd157f 100644 --- a/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs +++ b/osu.Game/Skinning/Editor/SkinEditorSceneLibrary.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; using osu.Game.Screens.Select; using osuTK; @@ -94,7 +95,7 @@ namespace osu.Game.Skinning.Editor var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod(); if (replayGeneratingMod != null) - screen.Push(new PlayerLoader(() => new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateReplayScore(beatmap, mods)))); + screen.Push(new PlayerLoader(() => new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods)))); }, new[] { typeof(Player), typeof(SongSelect) }) }, } diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index d463905cf4..66a956ca3d 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual if (autoplayMod != null) { - DrawableRuleset?.SetReplayScore(autoplayMod.CreateReplayScore(GameplayState.Beatmap, Mods.Value)); + DrawableRuleset?.SetReplayScore(autoplayMod.CreateScoreFromReplayData(GameplayState.Beatmap, Mods.Value)); return; } diff --git a/osu.Game/Tests/Visual/TestReplayPlayer.cs b/osu.Game/Tests/Visual/TestReplayPlayer.cs index da302d018d..bacb2427b0 100644 --- a/osu.Game/Tests/Visual/TestReplayPlayer.cs +++ b/osu.Game/Tests/Visual/TestReplayPlayer.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual /// Instantiate a replay player that renders an autoplay mod. /// public TestReplayPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) - : base((beatmap, mods) => mods.OfType().First().CreateReplayScore(beatmap, mods), new PlayerConfiguration + : base((beatmap, mods) => mods.OfType().First().CreateScoreFromReplayData(beatmap, mods), new PlayerConfiguration { AllowPause = allowPause, ShowResults = showResults From 4b2c01a8c1591c480f092fe5473f39c6aa3e185b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Mar 2022 16:59:03 +0900 Subject: [PATCH 157/285] Bring all mod implementations up-to-date --- .../Mods/EmptyFreeformModAutoplay.cs | 16 ++------------ .../Mods/PippidonModAutoplay.cs | 16 ++------------ .../Mods/EmptyScrollingModAutoplay.cs | 22 +++++-------------- .../Mods/PippidonModAutoplay.cs | 16 ++------------ .../Mods/CatchModAutoplay.cs | 16 ++------------ .../Mods/CatchModCinema.cs | 16 ++------------ .../Mods/ManiaModAutoplay.cs | 16 ++------------ .../Mods/ManiaModCinema.cs | 16 ++------------ .../TestSceneMissHitWindowJudgements.cs | 9 ++------ osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs | 16 ++------------ osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs | 16 ++------------ .../Mods/TaikoModAutoplay.cs | 16 ++------------ .../Mods/TaikoModCinema.cs | 16 ++------------ 13 files changed, 29 insertions(+), 178 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs index 9c2a108c4e..dfa0615604 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs @@ -3,26 +3,14 @@ using System.Collections.Generic; using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.EmptyFreeform.Replays; using osu.Game.Rulesets.Mods; -using osu.Game.Scoring; namespace osu.Game.Rulesets.EmptyFreeform.Mods { public class EmptyFreeformModAutoplay : ModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score - { - ScoreInfo = new ScoreInfo - { - User = new APIUser - { - Id = APIUser.SYSTEM_USER_ID, - Username = "sample" - }, - }, - Replay = new EmptyFreeformAutoGenerator(beatmap).Generate(), - }; + public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + => new ModReplayData(new EmptyFreeformAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "sample" }); } } diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs index 6683856a1a..6a24812ce8 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs @@ -3,26 +3,14 @@ using System.Collections.Generic; using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Pippidon.Replays; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Pippidon.Mods { public class PippidonModAutoplay : ModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score - { - ScoreInfo = new ScoreInfo - { - User = new APIUser - { - Id = APIUser.SYSTEM_USER_ID, - Username = "sample" - }, - }, - Replay = new PippidonAutoGenerator(beatmap).Generate(), - }; + public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + => new ModReplayData(new PippidonAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "sample" }); } } diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs index e3cecc7b44..cd0233fb51 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs @@ -1,28 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.EmptyScrolling.Replays; -using osu.Game.Scoring; using System.Collections.Generic; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.EmptyScrolling.Replays; +using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.EmptyScrolling.Mods { public class EmptyScrollingModAutoplay : ModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score - { - ScoreInfo = new ScoreInfo - { - User = new APIUser - { - Id = APIUser.SYSTEM_USER_ID, - Username = "sample" - }, - }, - Replay = new EmptyScrollingAutoGenerator(beatmap).Generate(), - }; + public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + => new ModReplayData(new EmptyScrollingAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "sample" }); } } diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs index 6683856a1a..6a24812ce8 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs @@ -3,26 +3,14 @@ using System.Collections.Generic; using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Pippidon.Replays; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Pippidon.Mods { public class PippidonModAutoplay : ModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score - { - ScoreInfo = new ScoreInfo - { - User = new APIUser - { - Id = APIUser.SYSTEM_USER_ID, - Username = "sample" - }, - }, - Replay = new PippidonAutoGenerator(beatmap).Generate(), - }; + public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + => new ModReplayData(new PippidonAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "sample" }); } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs index fbbb610035..08deb8378a 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs @@ -3,26 +3,14 @@ using System.Collections.Generic; using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Mods; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Catch.Mods { public class CatchModAutoplay : ModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score - { - ScoreInfo = new ScoreInfo - { - User = new APIUser - { - Id = APIUser.SYSTEM_USER_ID, - Username = "osu!salad" - } - }, - Replay = new CatchAutoGenerator(beatmap).Generate(), - }; + public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + => new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "osu!salad" }); } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs index 2f4769048f..d499348a09 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs @@ -3,27 +3,15 @@ using System.Collections.Generic; using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Mods; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Catch.Mods { public class CatchModCinema : ModCinema { - public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score - { - ScoreInfo = new ScoreInfo - { - User = new APIUser - { - Id = APIUser.SYSTEM_USER_ID, - Username = "osu!salad" - } - }, - Replay = new CatchAutoGenerator(beatmap).Generate(), - }; + public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + => new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "osu!salad" }); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs index 4eea0d4a44..9ea7852808 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs @@ -3,27 +3,15 @@ using System.Collections.Generic; using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mods; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModAutoplay : ModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score - { - ScoreInfo = new ScoreInfo - { - User = new APIUser - { - Id = APIUser.SYSTEM_USER_ID, - Username = "osu!topus" - } - }, - Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), - }; + public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + => new ModReplayData(new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), new ModCreatedReplayUser { Username = "osu!topus" }); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs index 0aa419bbcb..a5d33261c5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs @@ -3,28 +3,16 @@ using System.Collections.Generic; using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mods; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModCinema : ModCinema { - public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score - { - ScoreInfo = new ScoreInfo - { - User = new APIUser - { - Id = APIUser.SYSTEM_USER_ID, - Username = "osu!topus" - } - }, - Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), - }; + public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + => new ModReplayData(new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), new ModCreatedReplayUser { Username = "osu!topus" }); } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index 840d871b7b..124a519ab3 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Beatmaps; @@ -13,7 +12,6 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; using osu.Game.Tests.Visual; using osuTK; @@ -67,11 +65,8 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestAutoMod : OsuModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score - { - ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } }, - Replay = new MissingAutoGenerator(beatmap, mods).Generate() - }; + public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + => new ModReplayData(new MissingAutoGenerator(beatmap, mods).Generate(), new ModCreatedReplayUser { Username = "Autoplay" }); } private class MissingAutoGenerator : OsuAutoGeneratorBase diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs index c5b1c55c40..51283e171b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs @@ -5,10 +5,8 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Replays; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Mods { @@ -16,17 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); - public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score - { - ScoreInfo = new ScoreInfo - { - User = new APIUser - { - Id = APIUser.SYSTEM_USER_ID, - Username = "Autoplay", - } - }, - Replay = new OsuAutoGenerator(beatmap, mods).Generate() - }; + public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedReplayUser { Username = "Autoplay" }); } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs index 83e6f0efb5..6ecf8b73d2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs @@ -5,11 +5,9 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Mods { @@ -17,17 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); - public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score - { - ScoreInfo = new ScoreInfo - { - User = new APIUser - { - Id = APIUser.SYSTEM_USER_ID, - Username = "Autoplay" - } - }, - Replay = new OsuAutoGenerator(beatmap, mods).Generate() - }; + public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedReplayUser { Username = "Autoplay" }); } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs index b9bf3ab1b4..1579a121b6 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs @@ -3,26 +3,14 @@ using System.Collections.Generic; using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Replays; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModAutoplay : ModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score - { - ScoreInfo = new ScoreInfo - { - User = new APIUser - { - Id = APIUser.SYSTEM_USER_ID, - Username = "mekkadosu!" - } - }, - Replay = new TaikoAutoGenerator(beatmap).Generate(), - }; + public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + => new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "mekkadosu!" }); } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs index 6ecf8a3b3d..831e182193 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs @@ -3,27 +3,15 @@ using System.Collections.Generic; using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Replays; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModCinema : ModCinema { - public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score - { - ScoreInfo = new ScoreInfo - { - User = new APIUser - { - Id = APIUser.SYSTEM_USER_ID, - Username = "mekkadosu!" - } - }, - Replay = new TaikoAutoGenerator(beatmap).Generate(), - }; + public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) + => new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "mekkadosu!" }); } } From 1fb3d11591858b9f0e05e35f41e4fbf4840b3b0e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Mar 2022 18:00:56 +0900 Subject: [PATCH 158/285] Add ability to "migrate" data to another folder which has an existing install Until now, migrating would always attempt to move files. There's a chance that a user is reinstalling osu! but has their data at a custom location. We want to allow the chance for them to continue using the external data. This seems like the easiest way to make it work. Would be nice if we had a `Game.Restart()` method, but maybe this is enough for now? Note that further down the road we will probably prompt the user to potentially select a custom install path (including one with existing data) before osu! gets to writing anything. --- osu.Game/IO/OsuStorage.cs | 17 +++++++--- .../Maintenance/MigrationSelectScreen.cs | 32 ++++++++++++++++++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 6e7cb545e3..a3f7b4bfec 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -64,12 +64,22 @@ namespace osu.Game.IO /// public void ResetCustomStoragePath() { - storageConfig.SetValue(StorageConfig.FullPath, string.Empty); - storageConfig.Save(); + ChangeDataPath(string.Empty); ChangeTargetStorage(defaultStorage); } + /// + /// Updates the target data path without immediately switching. + /// This does NOT migrate any data. + /// The game should immediately be restarted after calling this. + /// + public void ChangeDataPath(string newPath) + { + storageConfig.SetValue(StorageConfig.FullPath, newPath); + storageConfig.Save(); + } + /// /// Attempts to change to the user's custom storage path. /// @@ -117,8 +127,7 @@ namespace osu.Game.IO { bool cleanupSucceeded = base.Migrate(newStorage); - storageConfig.SetValue(StorageConfig.FullPath, newStorage.GetFullPath(".")); - storageConfig.Save(); + ChangeDataPath(newStorage.GetFullPath(".")); return cleanupSucceeded; } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs index 1a60ab0638..9b20f2c0c3 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs @@ -3,11 +3,14 @@ using System; using System.IO; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Game.IO; +using osu.Game.Overlays.Dialog; namespace osu.Game.Overlays.Settings.Sections.Maintenance { @@ -16,6 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance [Resolved] private Storage storage { get; set; } + [Resolved] + private OsuGameBase game { get; set; } + + [Resolved(canBeNull: true)] + private DialogOverlay dialogOverlay { get; set; } + protected override DirectoryInfo InitialPath => new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent; public override bool AllowExternalScreenChange => false; @@ -32,8 +41,29 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance try { - if (target.GetDirectories().Length > 0 || target.GetFiles().Length > 0) + var directoryInfos = target.GetDirectories(); + var fileInfos = target.GetFiles(); + + if (directoryInfos.Length > 0 || fileInfos.Length > 0) + { + // Quick test for whether there's already an osu! install at the target path. + if (fileInfos.Any(f => f.Name == @"client.realm")) + { + dialogOverlay.Push(new ConfirmDialog("The target directory already seems to have an osu! install. Use this data instead?", () => + { + dialogOverlay.Push(new ConfirmDialog("To complete this operation, osu! will close. Please open it again to use the new data location.", () => + { + (storage as OsuStorage)?.ChangeDataPath(target.FullName); + game.GracefullyExit(); + }, () => { })); + }, + () => { })); + + return; + } + target = target.CreateSubdirectory("osu-lazer"); + } } catch (Exception e) { From 4741679a9404f08d79e0c412bc0f641b5c7d8c86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Mar 2022 19:03:50 +0900 Subject: [PATCH 159/285] Change confirmation message to be more clear about intentions Co-authored-by: Henry Lin --- .../Settings/Sections/Maintenance/MigrationSelectScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs index 9b20f2c0c3..047587e084 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs @@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance // Quick test for whether there's already an osu! install at the target path. if (fileInfos.Any(f => f.Name == @"client.realm")) { - dialogOverlay.Push(new ConfirmDialog("The target directory already seems to have an osu! install. Use this data instead?", () => + dialogOverlay.Push(new ConfirmDialog("The target directory already seems to have an osu! install. Use that data instead?", () => { dialogOverlay.Push(new ConfirmDialog("To complete this operation, osu! will close. Please open it again to use the new data location.", () => { From 9a09c97457bfbd233ee29f961d5326c4ee054b41 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 29 Mar 2022 16:11:44 +0300 Subject: [PATCH 160/285] Fix "Barrel Roll" tooltip not limiting decimal places for spin speed --- osu.Game/Rulesets/Mods/ModBarrelRoll.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs index 4acbcf3e74..bacb953f76 100644 --- a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs +++ b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mods public override string Description => "The whole playfield is on a wheel!"; public override double ScoreMultiplier => 1; - public override string SettingDescription => $"{SpinSpeed.Value} rpm {Direction.Value.GetDescription().ToLowerInvariant()}"; + public override string SettingDescription => $"{SpinSpeed.Value:N2} rpm {Direction.Value.GetDescription().ToLowerInvariant()}"; public void Update(Playfield playfield) { From 8d4356f23bf60cba89d1b7aa67513d38f3129b27 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 29 Mar 2022 17:25:05 +0300 Subject: [PATCH 161/285] Mark "autoplay" and "cinema" mods as mutually exclusive --- osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 +- osu.Game/Rulesets/Mods/ModCinema.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 60b9c29fe0..e804cb6734 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods public override bool UserPlayable => false; - public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) }; + public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) }; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 1fe5ddecf9..99c4e71d1f 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Objects; @@ -27,6 +29,8 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.ModCinema; public override string Description => "Watch the video without visual distractions."; + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAutoplay)).ToArray(); + public void ApplyToHUD(HUDOverlay overlay) { overlay.ShowHud.Value = false; From f762af13446554683388ed420bf97b0c9c6d807c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Mar 2022 23:28:07 +0900 Subject: [PATCH 162/285] Add test coverage of migrating to folder with existing data --- .../NonVisual/CustomDataDirectoryTest.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 834930a05e..b5ab33b9fc 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -233,6 +233,48 @@ namespace osu.Game.Tests.NonVisual } } + [Test] + public void TestMigrationFailsOnExistingData() + { + string customPath = prepareCustomPath(); + string customPath2 = prepareCustomPath(); + + using (var host = new CustomTestHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host); + + var storage = osu.Dependencies.Get(); + var osuStorage = storage as OsuStorage; + + string originalDirectory = storage.GetFullPath("."); + + const string database_filename = "client.realm"; + + Assert.DoesNotThrow(() => osu.Migrate(customPath)); + Assert.That(File.Exists(Path.Combine(customPath, database_filename))); + + Directory.CreateDirectory(customPath2); + File.Copy(Path.Combine(customPath, database_filename), Path.Combine(customPath2, database_filename)); + + // Fails because file already exists. + Assert.Throws(() => osu.Migrate(customPath2)); + + osuStorage?.ChangeDataPath(customPath2); + + Assert.That(osuStorage?.CustomStoragePath, Is.EqualTo(customPath2)); + Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath2}")); + } + finally + { + host.Exit(); + cleanupPath(customPath); + cleanupPath(customPath2); + } + } + } + [Test] public void TestMigrationToNestedTargetFails() { From f049d7cb677e899f9984611ae19410b01a18c978 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Tue, 29 Mar 2022 21:36:08 +0100 Subject: [PATCH 163/285] Implement `ChatTextBox` for new chat design Reference design: https://www.figma.com/file/f8b2dHp9LJCMOqYP4mdrPZ/Client%2FChat?node-id=1%3A297 Adds new component `ChatTextBox`. Exposes `BindableBool` `ShowSearch` to change text input behaviour between normal and search behaviour. Adds new component `ChatTextBar`. Exposes `BindableBool` `ShowSearch` which toggles between showing current chat channel or search icon. Additionally binds to child `ChatTextBox` components. Requires a cached `Bindable` instance to be managed by a parent component. --- InspectCode.sh | 2 +- .../Visual/Online/TestSceneChatTextBox.cs | 96 ++++++++++ osu.Game/Overlays/Chat/ChatTextBar.cs | 174 ++++++++++++++++++ osu.Game/Overlays/Chat/ChatTextBox.cs | 36 ++++ 4 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs create mode 100644 osu.Game/Overlays/Chat/ChatTextBar.cs create mode 100644 osu.Game/Overlays/Chat/ChatTextBox.cs diff --git a/InspectCode.sh b/InspectCode.sh index cf2bc18175..5a72324dd4 100755 --- a/InspectCode.sh +++ b/InspectCode.sh @@ -2,5 +2,5 @@ dotnet tool restore dotnet CodeFileSanity -dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN +dotnet jb inspectcode "osu.Game/osu.Game.csproj" --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs new file mode 100644 index 0000000000..e72a1d6652 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; +using osu.Game.Overlays; +using osu.Game.Overlays.Chat; + +namespace osu.Game.Tests.Visual.Online +{ + [TestFixture] + public class TestSceneChatTextBox : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); + + [Cached] + private readonly Bindable currentChannel = new Bindable(); + + private OsuSpriteText commitText; + private ChatTextBar bar; + + [SetUp] + public void SetUp() + { + Schedule(() => + { + Child = new GridContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 30), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + commitText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Default.With(size: 20), + }, + }, + new Drawable[] + { + bar = new ChatTextBar + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 0.99f, + }, + }, + }, + }; + + bar.TextBox.OnCommit += (sender, newText) => + { + commitText.Text = $"Commit: {sender.Text}"; + commitText.FadeOutFromOne(1000, Easing.InQuint); + sender.Text = string.Empty; + }; + }); + } + + [Test] + public void TestVisual() + { + AddStep("Public Channel", () => currentChannel.Value = createPublicChannel("#osu")); + AddStep("Public Channel Long Name", () => currentChannel.Value = createPublicChannel("#public-channel-long-name")); + AddStep("Private Channel", () => currentChannel.Value = createPrivateChannel("peppy", 2)); + AddStep("Private Long Name", () => currentChannel.Value = createPrivateChannel("test user long name", 3)); + + AddStep("Chat Mode Channel", () => bar.ShowSearch.Value = false); + AddStep("Chat Mode Search", () => bar.ShowSearch.Value = true); + } + + private static Channel createPublicChannel(string name) + => new Channel { Name = name, Type = ChannelType.Public, Id = 1234 }; + + private static Channel createPrivateChannel(string username, int id) + => new Channel(new APIUser { Id = id, Username = username }); + } +} diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs new file mode 100644 index 0000000000..00284fdd33 --- /dev/null +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -0,0 +1,174 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; +using osuTK; + +namespace osu.Game.Overlays.Chat +{ + public class ChatTextBar : Container + { + public readonly BindableBool ShowSearch = new BindableBool(); + + public ChatTextBox TextBox => chatTextBox; + + [Resolved] + private Bindable currentChannel { get; set; } = null!; + + private OsuTextFlowContainer chattingTextContainer = null!; + private Container searchIconContainer = null!; + private ChatTextBox chatTextBox = null!; + private Container enterContainer = null!; + + private const float chatting_text_width = 180; + private const float search_icon_width = 40; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.X; + Height = 60; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + chattingTextContainer = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 20)) + { + Masking = true, + Width = chatting_text_width, + Padding = new MarginPadding { Left = 10 }, + RelativeSizeAxes = Axes.Y, + TextAnchor = Anchor.CentreRight, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = colourProvider.Background1, + }, + searchIconContainer = new Container + { + RelativeSizeAxes = Axes.Y, + Width = search_icon_width, + Child = new SpriteIcon + { + Icon = FontAwesome.Solid.Search, + Origin = Anchor.CentreRight, + Anchor = Anchor.CentreRight, + Size = new Vector2(20), + Margin = new MarginPadding { Right = 2 }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 5 }, + Child = chatTextBox = new ChatTextBox + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + ShowSearch = { BindTarget = ShowSearch }, + HoldFocus = true, + ReleaseFocusOnCommit = false, + }, + }, + enterContainer = new Container + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Masking = true, + BorderColour = colourProvider.Background1, + BorderThickness = 2, + CornerRadius = 10, + Margin = new MarginPadding { Right = 10 }, + Size = new Vector2(60, 30), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Transparent, + }, + new OsuSpriteText + { + Text = "Enter", + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = colourProvider.Background1, + Font = OsuFont.Torus.With(size: 20), + Margin = new MarginPadding { Bottom = 2 }, + }, + }, + }, + }, + }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ShowSearch.BindValueChanged(change => + { + if (change.NewValue) + { + chattingTextContainer.Hide(); + enterContainer.Hide(); + searchIconContainer.Show(); + } + else + { + chattingTextContainer.Show(); + enterContainer.Show(); + searchIconContainer.Hide(); + } + }, true); + + currentChannel.BindValueChanged(change => + { + Channel newChannel = change.NewValue; + switch (newChannel?.Type) + { + case ChannelType.Public: + chattingTextContainer.Text = $"chatting in {newChannel.Name}"; + break; + case ChannelType.PM: + chattingTextContainer.Text = $"chatting with {newChannel.Name}"; + break; + default: + chattingTextContainer.Text = ""; + break; + } + }, true); + } + } +} diff --git a/osu.Game/Overlays/Chat/ChatTextBox.cs b/osu.Game/Overlays/Chat/ChatTextBox.cs new file mode 100644 index 0000000000..35ed26cda3 --- /dev/null +++ b/osu.Game/Overlays/Chat/ChatTextBox.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Framework.Bindables; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Chat +{ + public class ChatTextBox : FocusedTextBox + { + public readonly BindableBool ShowSearch = new BindableBool(); + + public override bool HandleLeftRightArrows => !ShowSearch.Value; + + protected override void LoadComplete() + { + base.LoadComplete(); + + ShowSearch.BindValueChanged(change => + { + PlaceholderText = change.NewValue ? "type here to search" : "type here"; + Schedule(() => Text = string.Empty); + }, true); + } + + protected override void Commit() + { + if (ShowSearch.Value) + return; + + base.Commit(); + } + } +} From 06c32aa1362c5f31d0ca068f9b2cf799d00997ad Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Tue, 29 Mar 2022 22:50:24 +0100 Subject: [PATCH 164/285] Remove changes to `InspectCode.sh` --- InspectCode.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InspectCode.sh b/InspectCode.sh index 5a72324dd4..cf2bc18175 100755 --- a/InspectCode.sh +++ b/InspectCode.sh @@ -2,5 +2,5 @@ dotnet tool restore dotnet CodeFileSanity -dotnet jb inspectcode "osu.Game/osu.Game.csproj" --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN +dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors From cef1b93471572085a6ecf97d11b95319b532b675 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 30 Mar 2022 01:06:35 +0300 Subject: [PATCH 165/285] Improve behaviour of "Autoplay" shortcut during gameplay start This also opens up the way to adding shortcut for "Cinema" mod (Ctrl+Shift+Enter), but will leave adding that until there's a demand for it. --- osu.Game/Screens/Select/PlaySongSelect.cs | 50 +++++++++++------------ 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 94aa165785..434a0ea030 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; @@ -14,13 +15,13 @@ using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Users; +using osu.Game.Utils; using osuTK.Input; namespace osu.Game.Screens.Select { public class PlaySongSelect : SongSelect { - private bool removeAutoModOnResume; private OsuScreen playerLoader; [Resolved(CanBeNull = true)] @@ -43,25 +44,6 @@ namespace osu.Game.Screens.Select protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - private ModAutoplay getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod(); - - public override void OnResuming(IScreen last) - { - base.OnResuming(last); - - playerLoader = null; - - if (removeAutoModOnResume) - { - var autoType = getAutoplayMod()?.GetType(); - - if (autoType != null) - Mods.Value = Mods.Value.Where(m => m.GetType() != autoType).ToArray(); - - removeAutoModOnResume = false; - } - } - protected override bool OnKeyDown(KeyDownEvent e) { switch (e.Key) @@ -77,10 +59,16 @@ namespace osu.Game.Screens.Select return base.OnKeyDown(e); } + private IReadOnlyList modsAtGameplayStart; + + private ModAutoplay getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod(); + protected override bool OnStart() { if (playerLoader != null) return false; + modsAtGameplayStart = Mods.Value; + // Ctrl+Enter should start map with autoplay enabled. if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true) { @@ -95,13 +83,12 @@ namespace osu.Game.Screens.Select return false; } - var mods = Mods.Value; + var mods = Mods.Value.Append(autoInstance).ToArray(); - if (mods.All(m => m.GetType() != autoInstance.GetType())) - { - Mods.Value = mods.Append(autoInstance).ToArray(); - removeAutoModOnResume = true; - } + if (!ModUtils.CheckCompatibleSet(mods, out var invalid)) + mods = mods.Except(invalid).Append(autoInstance).ToArray(); + + Mods.Value = mods; } SampleConfirm?.Play(); @@ -118,5 +105,16 @@ namespace osu.Game.Screens.Select return new SoloPlayer(); } } + + public override void OnResuming(IScreen last) + { + base.OnResuming(last); + + if (playerLoader != null) + { + Mods.Value = modsAtGameplayStart; + playerLoader = null; + } + } } } From b7ae431252d0cf5b537b312f9b613f4115c77d69 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 30 Mar 2022 01:08:22 +0300 Subject: [PATCH 166/285] Add test coverage --- .../SongSelect/TestScenePlaySongSelect.cs | 62 ++++++++++++++++++- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index c0c1e6b7a4..d27f16a624 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -68,7 +68,9 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("reset defaults", () => { Ruleset.Value = new OsuRuleset().RulesetInfo; + Beatmap.SetDefault(); + SelectedMods.SetDefault(); songSelect = null; }); @@ -563,7 +565,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestAutoplayViaCtrlEnter() + public void TestAutoplayShortcut() { addRulesetImportStep(0); @@ -580,11 +582,65 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader); - AddAssert("autoplay enabled", () => songSelect.Mods.Value.FirstOrDefault() is ModAutoplay); + AddAssert("autoplay selected", () => songSelect.Mods.Value.Single() is ModAutoplay); AddUntilStep("wait for return to ss", () => songSelect.IsCurrentScreen()); - AddAssert("mod disabled", () => songSelect.Mods.Value.Count == 0); + AddAssert("no mods selected", () => songSelect.Mods.Value.Count == 0); + } + + [Test] + public void TestAutoplayShortcutKeepsAutoplayIfSelectedAlready() + { + addRulesetImportStep(0); + + createSongSelect(); + + AddUntilStep("wait for selection", () => !Beatmap.IsDefault); + + changeMods(new OsuModAutoplay()); + + AddStep("press ctrl+enter", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Enter); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader); + + AddAssert("autoplay selected", () => songSelect.Mods.Value.Single() is ModAutoplay); + + AddUntilStep("wait for return to ss", () => songSelect.IsCurrentScreen()); + + AddAssert("autoplay still selected", () => songSelect.Mods.Value.Single() is ModAutoplay); + } + + [Test] + public void TestAutoplayShortcutReturnsInitialModsOnExit() + { + addRulesetImportStep(0); + + createSongSelect(); + + AddUntilStep("wait for selection", () => !Beatmap.IsDefault); + + changeMods(new OsuModRelax()); + + AddStep("press ctrl+enter", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Enter); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader); + + AddAssert("only autoplay selected", () => songSelect.Mods.Value.Single() is ModAutoplay); + + AddUntilStep("wait for return to ss", () => songSelect.IsCurrentScreen()); + + AddAssert("relax returned", () => songSelect.Mods.Value.Single() is ModRelax); } [Test] From 986aad4bd56c8f427753a06e6aaca09c5a427a86 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 30 Mar 2022 01:15:22 +0300 Subject: [PATCH 167/285] Fix potential nullref on `OsuGameTestScene.TearDownSteps` --- osu.Game/Tests/Visual/OsuGameTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index 3b8d9a4cd1..34d7723fa3 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual [TearDownSteps] public void TearDownSteps() { - if (DebugUtils.IsNUnitRunning) + if (DebugUtils.IsNUnitRunning && Game != null) { AddStep("exit game", () => Game.Exit()); AddUntilStep("wait for game exit", () => Game.Parent == null); From 7495a04990a01792b7e65b7a473eb41bcb70c5ed Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 30 Mar 2022 01:16:10 +0300 Subject: [PATCH 168/285] Ignore EF-to-realm migration tests on ARM architectures --- .../Navigation/TestEFToRealmMigration.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs b/osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs index 00a06d420e..8498b9b28f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs +++ b/osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs @@ -3,7 +3,9 @@ using System.IO; using System.Linq; +using System.Runtime.InteropServices; using NUnit.Framework; +using osu.Framework; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Database; @@ -28,6 +30,23 @@ namespace osu.Game.Tests.Visual.Navigation stream.CopyTo(outStream); } + [SetUp] + public void SetUp() + { + if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && RuntimeInformation.OSArchitecture == Architecture.Arm64) + Assert.Ignore("EF-to-realm migrations are not supported on M1 ARM architectures."); + } + + public override void SetUpSteps() + { + // base SetUpSteps are executed before the above SetUp, therefore early-return to allow ignoring test properly. + // attempting to ignore here would yield a TargetInvocationException instead. + if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && RuntimeInformation.OSArchitecture == Architecture.Arm64) + return; + + base.SetUpSteps(); + } + [Test] public void TestMigration() { From 7582c943a4a77e92d9dd3504dcdb29e755132b82 Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Wed, 30 Mar 2022 00:48:59 +0200 Subject: [PATCH 169/285] Use In/Out instead of {In/Out}Quad --- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index afcdf5d07f..07dd42e173 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -207,8 +207,8 @@ namespace osu.Game.Screens.Play.HUD counterContainer.Size = displayedCountSpriteText.Size; displayedCountSpriteText.ScaleTo(1).Then() - .ScaleTo(1.1f, small_pop_out_duration / 2, Easing.InQuad).Then() - .ScaleTo(1, small_pop_out_duration / 2, Easing.OutQuad); + .ScaleTo(1.1f, small_pop_out_duration / 2, Easing.In).Then() + .ScaleTo(1, small_pop_out_duration / 2, Easing.Out); } private void scheduledPopOutSmall(uint id) From cb62d3d4b9528336b0a8a950a05cb7f0b271851a Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Wed, 30 Mar 2022 01:09:05 +0200 Subject: [PATCH 170/285] Remove skin dependency as component is reinitialized on skin change --- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 07dd42e173..eaca623e39 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -44,9 +44,6 @@ namespace osu.Game.Screens.Play.HUD private readonly Container counterContainer; - [Resolved] - private ISkinSource skin { get; set; } - /// /// Hides the combo counter internally without affecting its . /// @@ -135,8 +132,6 @@ namespace osu.Game.Screens.Play.HUD Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true); - // Since layout depends on combo font height we need to update it during skin change - skin.SourceChanged += updateLayout; updateLayout(); } @@ -292,13 +287,5 @@ namespace osu.Game.Screens.Play.HUD double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue; return difference * rolling_duration; } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (skin != null) - skin.SourceChanged -= updateLayout; - } } } From e7d2d94eeef6b11cc13660855b8267247ffc202c Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Wed, 30 Mar 2022 02:16:50 +0100 Subject: [PATCH 171/285] Fix code quality issues in `ChatTextBar` --- osu.Game/Overlays/Chat/ChatTextBar.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 00284fdd33..66f9f281c9 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -21,14 +21,13 @@ namespace osu.Game.Overlays.Chat { public readonly BindableBool ShowSearch = new BindableBool(); - public ChatTextBox TextBox => chatTextBox; + public ChatTextBox TextBox { get; private set; } = null!; [Resolved] private Bindable currentChannel { get; set; } = null!; private OsuTextFlowContainer chattingTextContainer = null!; private Container searchIconContainer = null!; - private ChatTextBox chatTextBox = null!; private Container enterContainer = null!; private const float chatting_text_width = 180; @@ -89,7 +88,7 @@ namespace osu.Game.Overlays.Chat { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Right = 5 }, - Child = chatTextBox = new ChatTextBox + Child = TextBox = new ChatTextBox { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -156,16 +155,19 @@ namespace osu.Game.Overlays.Chat currentChannel.BindValueChanged(change => { Channel newChannel = change.NewValue; + switch (newChannel?.Type) { case ChannelType.Public: chattingTextContainer.Text = $"chatting in {newChannel.Name}"; break; + case ChannelType.PM: chattingTextContainer.Text = $"chatting with {newChannel.Name}"; break; + default: - chattingTextContainer.Text = ""; + chattingTextContainer.Text = string.Empty; break; } }, true); From bd9c0076df59f0317fd9d43c69741ab2c5ef7044 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 30 Mar 2022 13:03:10 +0900 Subject: [PATCH 172/285] Fix scores sometimes shown with incorrect scoring mode --- osu.Game/Scoring/ScoreManager.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 83359838aa..113886dbd7 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -118,7 +118,11 @@ namespace osu.Game.Scoring public void GetTotalScore([NotNull] ScoreInfo score, [NotNull] Action callback, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default) { GetTotalScoreAsync(score, mode, cancellationToken) - .ContinueWith(task => scheduler.Add(() => callback(task.GetResultSafely())), TaskContinuationOptions.OnlyOnRanToCompletion); + .ContinueWith(task => scheduler.Add(() => + { + if (!cancellationToken.IsCancellationRequested) + callback(task.GetResultSafely()); + }), TaskContinuationOptions.OnlyOnRanToCompletion); } /// From 4df63a4900897c6811a2a233b1602acbe3be6bb0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 30 Mar 2022 13:15:41 +0900 Subject: [PATCH 173/285] Prevent scores being calculated twice --- osu.Game/Scoring/ScoreManager.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 113886dbd7..fbec6ea1fb 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -90,12 +90,7 @@ namespace osu.Game.Scoring /// /// The to retrieve the bindable for. /// The bindable containing the total score. - public Bindable GetBindableTotalScore([NotNull] ScoreInfo score) - { - var bindable = new TotalScoreBindable(score, this); - configManager?.BindWith(OsuSetting.ScoreDisplayMode, bindable.ScoringMode); - return bindable; - } + public Bindable GetBindableTotalScore([NotNull] ScoreInfo score) => new TotalScoreBindable(score, this, configManager); /// /// Retrieves a bindable that represents the formatted total score string of a . @@ -187,8 +182,7 @@ namespace osu.Game.Scoring /// private class TotalScoreBindable : Bindable { - public readonly Bindable ScoringMode = new Bindable(); - + private readonly Bindable scoringMode = new Bindable(); private readonly ScoreInfo score; private readonly ScoreManager scoreManager; @@ -199,12 +193,14 @@ namespace osu.Game.Scoring /// /// The to provide the total score of. /// The . - public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager) + /// The config. + public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager, OsuConfigManager configManager) { this.score = score; this.scoreManager = scoreManager; - ScoringMode.BindValueChanged(onScoringModeChanged, true); + configManager?.BindWith(OsuSetting.ScoreDisplayMode, scoringMode); + scoringMode.BindValueChanged(onScoringModeChanged, true); } private void onScoringModeChanged(ValueChangedEvent mode) From 975883da5c4ce921ee48319ee000c4f901f6a3c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Mar 2022 13:34:48 +0900 Subject: [PATCH 174/285] Move all usages of `client.realm` filename to a single `const` --- osu.Game.Benchmarks/BenchmarkRealmReads.cs | 2 +- osu.Game.Tests/Database/RealmTest.cs | 4 ++-- .../NonVisual/CustomDataDirectoryTest.cs | 20 ++++++++----------- osu.Game/Database/RealmAccess.cs | 2 +- osu.Game/IO/OsuStorage.cs | 6 +++--- osu.Game/OsuGameBase.cs | 7 ++++++- .../Maintenance/MigrationSelectScreen.cs | 2 +- osu.Game/Tests/Visual/OsuTestScene.cs | 2 +- 8 files changed, 23 insertions(+), 22 deletions(-) diff --git a/osu.Game.Benchmarks/BenchmarkRealmReads.cs b/osu.Game.Benchmarks/BenchmarkRealmReads.cs index bf9467700c..615e2e964d 100644 --- a/osu.Game.Benchmarks/BenchmarkRealmReads.cs +++ b/osu.Game.Benchmarks/BenchmarkRealmReads.cs @@ -27,7 +27,7 @@ namespace osu.Game.Benchmarks storage = new TemporaryNativeStorage("realm-benchmark"); storage.DeleteDirectory(string.Empty); - realm = new RealmAccess(storage, "client"); + realm = new RealmAccess(storage, OsuGameBase.CLIENT_DATABASE_FILENAME); realm.Run(r => { diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index 838759c991..16072888b9 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Database // ReSharper disable once AccessToDisposedClosure var testStorage = new OsuStorage(host, storage.GetStorageForDirectory(caller)); - using (var realm = new RealmAccess(testStorage, "client")) + using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME)) { Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}"); testAction(realm, testStorage); @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Database { var testStorage = storage.GetStorageForDirectory(caller); - using (var realm = new RealmAccess(testStorage, "client")) + using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME)) { Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}"); await testAction(realm, testStorage); diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index b5ab33b9fc..fd5691a9f4 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -143,14 +143,14 @@ namespace osu.Game.Tests.NonVisual Assert.That(osuStorage, Is.Not.Null); // In the following tests, realm files are ignored as - // - in the case of checking the source, interacting with the pipe files (client.realm.note) may + // - in the case of checking the source, interacting with the pipe files (.realm.note) may // lead to unexpected behaviour. // - in the case of checking the destination, the files may have already been recreated by the game // as part of the standard migration flow. foreach (string file in osuStorage.IgnoreFiles) { - if (!file.Contains("realm", StringComparison.Ordinal)) + if (!file.Contains(".realm", StringComparison.Ordinal)) { Assert.That(File.Exists(Path.Combine(originalDirectory, file))); Assert.That(storage.Exists(file), Is.False, () => $"{file} exists in destination when it was expected to be ignored"); @@ -159,7 +159,7 @@ namespace osu.Game.Tests.NonVisual foreach (string dir in osuStorage.IgnoreDirectories) { - if (!dir.Contains("realm", StringComparison.Ordinal)) + if (!dir.Contains(".realm", StringComparison.Ordinal)) { Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir))); Assert.That(storage.Exists(dir), Is.False, () => $"{dir} exists in destination when it was expected to be ignored"); @@ -188,19 +188,17 @@ namespace osu.Game.Tests.NonVisual { var osu = LoadOsuIntoHost(host); - const string database_filename = "client.realm"; - Assert.DoesNotThrow(() => osu.Migrate(customPath)); - Assert.That(File.Exists(Path.Combine(customPath, database_filename))); + Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME))); Assert.DoesNotThrow(() => osu.Migrate(customPath2)); - Assert.That(File.Exists(Path.Combine(customPath2, database_filename))); + Assert.That(File.Exists(Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME))); // some files may have been left behind for whatever reason, but that's not what we're testing here. cleanupPath(customPath); Assert.DoesNotThrow(() => osu.Migrate(customPath)); - Assert.That(File.Exists(Path.Combine(customPath, database_filename))); + Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME))); } finally { @@ -250,13 +248,11 @@ namespace osu.Game.Tests.NonVisual string originalDirectory = storage.GetFullPath("."); - const string database_filename = "client.realm"; - Assert.DoesNotThrow(() => osu.Migrate(customPath)); - Assert.That(File.Exists(Path.Combine(customPath, database_filename))); + Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME))); Directory.CreateDirectory(customPath2); - File.Copy(Path.Combine(customPath, database_filename), Path.Combine(customPath2, database_filename)); + File.Copy(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME), Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME)); // Fails because file already exists. Assert.Throws(() => osu.Migrate(customPath2)); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 8dbb338980..b0a70b51d0 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -212,7 +212,7 @@ namespace osu.Game.Database if (realm.All().Any()) { Logger.Log(@"Recovery aborted as the existing database has scores set already.", LoggingTarget.Database); - Logger.Log(@"To perform recovery, delete client.realm while osu! is not running.", LoggingTarget.Database); + Logger.Log($@"To perform recovery, delete {OsuGameBase.CLIENT_DATABASE_FILENAME} while osu! is not running.", LoggingTarget.Database); return; } } diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index a3f7b4bfec..c49365a9de 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -36,15 +36,15 @@ namespace osu.Game.IO public override string[] IgnoreDirectories => new[] { "cache", - "client.realm.management" + $"{OsuGameBase.CLIENT_DATABASE_FILENAME}.management", }; public override string[] IgnoreFiles => new[] { "framework.ini", "storage.ini", - "client.realm.note", - "client.realm.lock", + $"{OsuGameBase.CLIENT_DATABASE_FILENAME}.note", + $"{OsuGameBase.CLIENT_DATABASE_FILENAME}.lock", }; public OsuStorage(GameHost host, Storage defaultStorage) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7b9aca4086..324fcada89 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -57,6 +57,11 @@ namespace osu.Game public const string CLIENT_STREAM_NAME = @"lazer"; + /// + /// The filename of the main client database. + /// + public const string CLIENT_DATABASE_FILENAME = @"client.realm"; + public const int SAMPLE_CONCURRENCY = 6; /// @@ -200,7 +205,7 @@ namespace osu.Game if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME)) dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage)); - dependencies.Cache(realm = new RealmAccess(Storage, "client", Host.UpdateThread, EFContextFactory)); + dependencies.Cache(realm = new RealmAccess(Storage, CLIENT_DATABASE_FILENAME, Host.UpdateThread, EFContextFactory)); dependencies.CacheAs(RulesetStore = new RealmRulesetStore(realm, Storage)); dependencies.CacheAs(RulesetStore); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs index 047587e084..0304a4291a 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance if (directoryInfos.Length > 0 || fileInfos.Length > 0) { // Quick test for whether there's already an osu! install at the target path. - if (fileInfos.Any(f => f.Name == @"client.realm")) + if (fileInfos.Any(f => f.Name == OsuGameBase.CLIENT_DATABASE_FILENAME)) { dialogOverlay.Push(new ConfirmDialog("The target directory already seems to have an osu! install. Use that data instead?", () => { diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 6c332c2408..f2d280417e 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual Resources = parent.Get().Resources; - realm = new Lazy(() => new RealmAccess(LocalStorage, "client", host.UpdateThread)); + realm = new Lazy(() => new RealmAccess(LocalStorage, OsuGameBase.CLIENT_DATABASE_FILENAME, host.UpdateThread)); RecycleLocalStorage(false); From bc0b982102a89c8e18ef46944c47e004ec82780b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Mar 2022 17:46:17 +0900 Subject: [PATCH 175/285] Remove song source from main wedge display This was definitely added at someone's request, since I wouldn't have put it here. But it's displayed below in the details section already and also not displayed in the updated "wedge" in the new design. See https://github.com/ppy/osu/discussions/17537 for discussion. --- .../Visual/SongSelect/TestSceneBeatmapInfoWedge.cs | 2 +- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index fd3f739c34..644a333fcf 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -116,7 +116,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void testBeatmapLabels(Ruleset ruleset) { AddAssert("check version", () => infoWedge.Info.VersionLabel.Current.Value == $"{ruleset.ShortName}Version"); - AddAssert("check title", () => infoWedge.Info.TitleLabel.Current.Value == $"{ruleset.ShortName}Source — {ruleset.ShortName}Title"); + AddAssert("check title", () => infoWedge.Info.TitleLabel.Current.Value == $"{ruleset.ShortName}Title"); AddAssert("check artist", () => infoWedge.Info.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist"); AddAssert("check author", () => infoWedge.Info.MapperContainer.ChildrenOfType().Any(s => s.Current.Value == $"{ruleset.ShortName}Author")); } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 1e0aaf9c27..1a5a09afa3 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -314,8 +314,8 @@ namespace osu.Game.Screens.Select } }; - titleBinding.BindValueChanged(_ => setMetadata(metadata.Source)); - artistBinding.BindValueChanged(_ => setMetadata(metadata.Source), true); + titleBinding.BindValueChanged(_ => setMetadata()); + artistBinding.BindValueChanged(_ => setMetadata(), true); addInfoLabels(); } @@ -352,10 +352,10 @@ namespace osu.Game.Screens.Select }, true); } - private void setMetadata(string source) + private void setMetadata() { ArtistLabel.Text = artistBinding.Value; - TitleLabel.Text = string.IsNullOrEmpty(source) ? titleBinding.Value : source + " — " + titleBinding.Value; + TitleLabel.Text = titleBinding.Value; } private void addInfoLabels() From f9f6248101a62841037d1afe294aa544cf58cfd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Mar 2022 17:59:45 +0900 Subject: [PATCH 176/285] Simplify string bindings --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 1a5a09afa3..7db1016f62 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -287,12 +287,14 @@ namespace osu.Game.Screens.Select { TitleLabel = new OsuSpriteText { + Current = { BindTarget = titleBinding }, Font = OsuFont.GetFont(size: 28, italics: true), RelativeSizeAxes = Axes.X, Truncate = true, }, ArtistLabel = new OsuSpriteText { + Current = { BindTarget = artistBinding }, Font = OsuFont.GetFont(size: 17, italics: true), RelativeSizeAxes = Axes.X, Truncate = true, @@ -314,9 +316,6 @@ namespace osu.Game.Screens.Select } }; - titleBinding.BindValueChanged(_ => setMetadata()); - artistBinding.BindValueChanged(_ => setMetadata(), true); - addInfoLabels(); } @@ -352,12 +351,6 @@ namespace osu.Game.Screens.Select }, true); } - private void setMetadata() - { - ArtistLabel.Text = artistBinding.Value; - TitleLabel.Text = titleBinding.Value; - } - private void addInfoLabels() { if (working.Beatmap?.HitObjects?.Any() != true) From 671386ba6cf29492d213d9cf19c5ad04754def65 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Mar 2022 20:35:11 +0900 Subject: [PATCH 177/285] Fix `MultiplayerMatchSongSelect` potentially attempting to exit when not the current screen --- .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index e30ec36e9c..d49c122bd1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -80,6 +80,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { Schedule(() => { + // If an error or server side trigger occurred this screen may have already exited by external means. + if (!this.IsCurrentScreen()) + return; + loadingLayer.Hide(); if (t.IsFaulted) From eec3fef7a66382337c2c2ee99c7f28ebb951407e Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Wed, 30 Mar 2022 20:25:23 +0100 Subject: [PATCH 178/285] Remove the enter box in `ChatTextBar` --- osu.Game/Overlays/Chat/ChatTextBar.cs | 32 --------------------------- 1 file changed, 32 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 66f9f281c9..7ff1b8d1d3 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -28,7 +28,6 @@ namespace osu.Game.Overlays.Chat private OsuTextFlowContainer chattingTextContainer = null!; private Container searchIconContainer = null!; - private Container enterContainer = null!; private const float chatting_text_width = 180; private const float search_icon_width = 40; @@ -54,7 +53,6 @@ namespace osu.Game.Overlays.Chat new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize), new Dimension(), - new Dimension(GridSizeMode.AutoSize), }, Content = new[] { @@ -98,34 +96,6 @@ namespace osu.Game.Overlays.Chat ReleaseFocusOnCommit = false, }, }, - enterContainer = new Container - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Masking = true, - BorderColour = colourProvider.Background1, - BorderThickness = 2, - CornerRadius = 10, - Margin = new MarginPadding { Right = 10 }, - Size = new Vector2(60, 30), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.Transparent, - }, - new OsuSpriteText - { - Text = "Enter", - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Colour = colourProvider.Background1, - Font = OsuFont.Torus.With(size: 20), - Margin = new MarginPadding { Bottom = 2 }, - }, - }, - }, }, }, }, @@ -141,13 +111,11 @@ namespace osu.Game.Overlays.Chat if (change.NewValue) { chattingTextContainer.Hide(); - enterContainer.Hide(); searchIconContainer.Show(); } else { chattingTextContainer.Show(); - enterContainer.Show(); searchIconContainer.Hide(); } }, true); From fff30e8a6ea20b9189f87547e86be90cd01ddfa7 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Wed, 30 Mar 2022 21:01:28 +0100 Subject: [PATCH 179/285] Simplify show/hide of text and search in `ChatTextBar` --- osu.Game/Overlays/Chat/ChatTextBar.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 7ff1b8d1d3..d7edbb83b6 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -108,16 +108,10 @@ namespace osu.Game.Overlays.Chat ShowSearch.BindValueChanged(change => { - if (change.NewValue) - { - chattingTextContainer.Hide(); - searchIconContainer.Show(); - } - else - { - chattingTextContainer.Show(); - searchIconContainer.Hide(); - } + bool showSearch = change.NewValue; + + chattingTextContainer.FadeTo(showSearch ? 0 : 1); + searchIconContainer.FadeTo(showSearch ? 1 : 0); }, true); currentChannel.BindValueChanged(change => From 83bae81095dd8de79cfef2f166db8eeb16c0873a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 30 Mar 2022 22:02:51 +0200 Subject: [PATCH 180/285] Fill out `ICreateReplayData` xmldocs --- osu.Game/Rulesets/Mods/ICreateReplayData.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ICreateReplayData.cs b/osu.Game/Rulesets/Mods/ICreateReplayData.cs index b022949345..1d07b95e7a 100644 --- a/osu.Game/Rulesets/Mods/ICreateReplayData.cs +++ b/osu.Game/Rulesets/Mods/ICreateReplayData.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; using osu.Game.Replays; +using osu.Game.Scoring; using osu.Game.Users; namespace osu.Game.Rulesets.Mods @@ -17,9 +18,13 @@ namespace osu.Game.Rulesets.Mods /// /// Create replay data. /// - /// - /// - /// + /// The beatmap to create replay data for. + /// The mods to take into account when creating the replay data. + /// A structure, containing the generated replay data. + /// + /// For callers that want to receive a directly usable instance, + /// the extension method is provided for convenience. + /// public ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods); } From 436dec68c993813c20a8ff6753f459390226e38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 30 Mar 2022 22:03:51 +0200 Subject: [PATCH 181/285] Use provided extension method instead of reimplementing locally --- osu.Game/Screens/Select/PlaySongSelect.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 3f2a5c6183..593436bbb7 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Game.Graphics; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Mods; @@ -103,23 +102,7 @@ namespace osu.Game.Screens.Select if (replayGeneratingMod != null) { - return new ReplayPlayer((beatmap, mods) => - { - var replayData = replayGeneratingMod.CreateReplayData(beatmap, mods); - - return new Score - { - Replay = replayData.Replay, - ScoreInfo = - { - User = new APIUser - { - Id = APIUser.SYSTEM_USER_ID, - Username = replayData.User.Username, - } - } - }; - }); + return new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods)); } return new SoloPlayer(); From f4184cb6fe5abc7ae7e86d7f0fecace52f4b010c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 30 Mar 2022 22:04:19 +0200 Subject: [PATCH 182/285] Fix typo in step name --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 518ab5a1d3..346a88a2d5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100); AddAssert("score has no misses", () => getResultsScreen().Score.Statistics[HitResult.Miss] == 0); - AddUntilStep("avatar dispalyed", () => getAvatar() != null); + AddUntilStep("avatar displayed", () => getAvatar() != null); AddAssert("avatar not clickable", () => getAvatar().ChildrenOfType().First().Action == null); ClickableAvatar getAvatar() => getResultsScreen() From 3ac0da2da305feac1ea30999e257027ffe37e51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Mar 2022 21:33:52 +0100 Subject: [PATCH 183/285] Implement sheared toggle button --- .../TestSceneShearedToggleButton.cs | 88 ++++++++++ .../UserInterface/ShearedToggleButton.cs | 155 ++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs create mode 100644 osu.Game/Graphics/UserInterface/ShearedToggleButton.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs new file mode 100644 index 0000000000..a969858157 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs @@ -0,0 +1,88 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestSceneShearedToggleButton : OsuManualInputManagerTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + + [Test] + public void TestShearedToggleButton() + { + ShearedToggleButton button = null; + + AddStep("create button", () => + { + Child = button = new ShearedToggleButton(0.2f) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Toggle me", + Width = 200 + }; + }); + + AddToggleStep("toggle button", active => button.Active.Value = active); + AddToggleStep("toggle disabled", disabled => button.Active.Disabled = disabled); + } + + [Test] + public void TestDisabledState() + { + ShearedToggleButton button = null; + + AddStep("create button", () => + { + Child = button = new ShearedToggleButton(0.2f) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Toggle me", + Width = 200 + }; + }); + + clickToggle(); + assertToggleState(true); + + clickToggle(); + assertToggleState(false); + + setToggleDisabledState(true); + + assertToggleState(false); + clickToggle(); + assertToggleState(false); + + setToggleDisabledState(false); + assertToggleState(false); + clickToggle(); + assertToggleState(true); + + setToggleDisabledState(true); + assertToggleState(true); + clickToggle(); + assertToggleState(true); + + void clickToggle() => AddStep("click toggle", () => + { + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + + void assertToggleState(bool active) => AddAssert($"toggle is {(active ? "" : "not ")}active", () => button.Active.Value == active); + + void setToggleDisabledState(bool disabled) => AddStep($"{(disabled ? "disable" : "enable")} toggle", () => button.Active.Disabled = disabled); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs new file mode 100644 index 0000000000..acbed29279 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs @@ -0,0 +1,155 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Graphics.UserInterface +{ + public class ShearedToggleButton : OsuClickableContainer + { + public BindableBool Active { get; } = new BindableBool(); + + public LocalisableString Text + { + get => text.Text; + set => text.Text = value; + } + + private readonly Box background; + private readonly OsuSpriteText text; + + private Sample? sampleOff; + private Sample? sampleOn; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public ShearedToggleButton(float shear) + { + Height = 50; + Padding = new MarginPadding { Horizontal = shear * 50 }; + + Content.CornerRadius = 7; + Content.Shear = new Vector2(shear, 0); + Content.Masking = true; + Content.BorderThickness = 2; + Content.Anchor = Content.Origin = Anchor.Centre; + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.TorusAlternate.With(size: 17), + Shear = new Vector2(-shear, 0) + } + }; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleOn = audio.Samples.Get(@"UI/check-on"); + sampleOff = audio.Samples.Get(@"UI/check-off"); + } + + protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet); + + protected override void LoadComplete() + { + base.LoadComplete(); + + Active.BindValueChanged(_ => + { + updateState(); + playSample(); + }); + Active.BindDisabledChanged(disabled => + { + updateState(); + Action = disabled ? (Action?)null : Active.Toggle; + }, true); + + FinishTransforms(true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + Content.ScaleTo(0.8f, 2000, Easing.OutQuint); + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + Content.ScaleTo(1, 1000, Easing.OutElastic); + base.OnMouseUp(e); + } + + private void updateState() + { + var darkerColour = Active.Value ? colourProvider.Highlight1 : colourProvider.Background3; + var lighterColour = Active.Value ? colourProvider.Colour0 : colourProvider.Background1; + + if (Active.Disabled) + { + darkerColour = darkerColour.Darken(0.3f); + lighterColour = lighterColour.Darken(0.3f); + } + else if (IsHovered) + { + darkerColour = darkerColour.Lighten(0.3f); + lighterColour = lighterColour.Lighten(0.3f); + } + + background.FadeColour(darkerColour, 150, Easing.OutQuint); + Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(darkerColour, lighterColour), 150, Easing.OutQuint); + + var textColour = Active.Value ? colourProvider.Background6 : colourProvider.Content1; + if (Active.Disabled) + textColour = textColour.Opacity(0.6f); + + text.FadeColour(textColour, 150, Easing.OutQuint); + } + + private void playSample() + { + if (Active.Value) + sampleOn?.Play(); + else + sampleOff?.Play(); + } + } +} From 6874cdf0c88db8d49f9545fc633eaab9a7ea307c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 31 Mar 2022 01:50:46 +0300 Subject: [PATCH 184/285] Remove unnecessary `public` prefix in interface method --- osu.Game/Rulesets/Mods/ICreateReplayData.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ICreateReplayData.cs b/osu.Game/Rulesets/Mods/ICreateReplayData.cs index 1d07b95e7a..2b8305ac78 100644 --- a/osu.Game/Rulesets/Mods/ICreateReplayData.cs +++ b/osu.Game/Rulesets/Mods/ICreateReplayData.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mods /// For callers that want to receive a directly usable instance, /// the extension method is provided for convenience. /// - public ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods); + ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods); } /// From 234bec45cca3a30c66ea13c630066b9fef1b6c43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 31 Mar 2022 11:32:00 +0900 Subject: [PATCH 185/285] Remove unnecessary logging --- osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 10fe97c180..7b9ae187ce 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -36,8 +36,6 @@ namespace osu.Game.Rulesets.Mods public virtual ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) { - Logger.Log($"Ruleset mod implementation for {GetType().Name} should be updated to newer {nameof(ICreateReplayData)} signature.", LoggingTarget.Information); - #pragma warning disable CS0618 var replayScore = CreateReplayScore(beatmap, mods); #pragma warning restore CS0618 From 2c1ccc7d365b26202949ce08ecf86d3146cae77d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 31 Mar 2022 11:33:26 +0900 Subject: [PATCH 186/285] Update obsolete message to match targeted developers' use case Co-authored-by: Salman Ahmed --- osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 7b9ae187ce..7e54c170bf 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mods public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; - [Obsolete("Use CreateScoreFromReplayData(IBeatmap, IReadOnlyList) instead")] // Can be removed 20220929 + [Obsolete("Override CreateReplayData(IBeatmap, IReadOnlyList) instead")] // Can be removed 20220929 public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { Replay = new Replay() }; public virtual ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) From 0cac9359397ae36e322a628ec7cc8c8c5a43e2f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 31 Mar 2022 11:34:23 +0900 Subject: [PATCH 187/285] Shorten class name of `ModCreatedReplayUser` --- .../Mods/EmptyFreeformModAutoplay.cs | 2 +- .../Mods/PippidonModAutoplay.cs | 2 +- .../Mods/EmptyScrollingModAutoplay.cs | 2 +- .../Mods/PippidonModAutoplay.cs | 2 +- osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs | 2 +- osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs | 2 +- osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs | 2 +- osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs | 2 +- .../TestSceneMissHitWindowJudgements.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs | 2 +- osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs | 2 +- osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs | 2 +- osu.Game/Rulesets/Mods/ICreateReplay.cs | 2 +- osu.Game/Rulesets/Mods/ICreateReplayData.cs | 8 ++++---- osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 +- 16 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs index dfa0615604..d3ef3f6e56 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Mods/EmptyFreeformModAutoplay.cs @@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.EmptyFreeform.Mods public class EmptyFreeformModAutoplay : ModAutoplay { public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) - => new ModReplayData(new EmptyFreeformAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "sample" }); + => new ModReplayData(new EmptyFreeformAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" }); } } diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs index 6a24812ce8..f57b874ff3 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs @@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Pippidon.Mods public class PippidonModAutoplay : ModAutoplay { public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) - => new ModReplayData(new PippidonAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "sample" }); + => new ModReplayData(new PippidonAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" }); } } diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs index cd0233fb51..5cf40c30cd 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/Mods/EmptyScrollingModAutoplay.cs @@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.EmptyScrolling.Mods public class EmptyScrollingModAutoplay : ModAutoplay { public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) - => new ModReplayData(new EmptyScrollingAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "sample" }); + => new ModReplayData(new EmptyScrollingAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" }); } } diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs index 6a24812ce8..f57b874ff3 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Mods/PippidonModAutoplay.cs @@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Pippidon.Mods public class PippidonModAutoplay : ModAutoplay { public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) - => new ModReplayData(new PippidonAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "sample" }); + => new ModReplayData(new PippidonAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" }); } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs index 08deb8378a..50e48101d3 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs @@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Catch.Mods public class CatchModAutoplay : ModAutoplay { public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) - => new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "osu!salad" }); + => new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" }); } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs index d499348a09..7eda6b37d3 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs @@ -12,6 +12,6 @@ namespace osu.Game.Rulesets.Catch.Mods public class CatchModCinema : ModCinema { public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) - => new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "osu!salad" }); + => new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" }); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs index 9ea7852808..d444c9b634 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs @@ -12,6 +12,6 @@ namespace osu.Game.Rulesets.Mania.Mods public class ManiaModAutoplay : ModAutoplay { public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) - => new ModReplayData(new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), new ModCreatedReplayUser { Username = "osu!topus" }); + => new ModReplayData(new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), new ModCreatedUser { Username = "osu!topus" }); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs index a5d33261c5..f0db742eac 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs @@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Mania.Mods public class ManiaModCinema : ModCinema { public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) - => new ModReplayData(new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), new ModCreatedReplayUser { Username = "osu!topus" }); + => new ModReplayData(new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), new ModCreatedUser { Username = "osu!topus" }); } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index 124a519ab3..a9325f98f7 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestAutoMod : OsuModAutoplay { public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) - => new ModReplayData(new MissingAutoGenerator(beatmap, mods).Generate(), new ModCreatedReplayUser { Username = "Autoplay" }); + => new ModReplayData(new MissingAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); } private class MissingAutoGenerator : OsuAutoGeneratorBase diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs index 51283e171b..31179cdf4a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs @@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) - => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedReplayUser { Username = "Autoplay" }); + => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs index 6ecf8b73d2..d677ab43d0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) - => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedReplayUser { Username = "Autoplay" }); + => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs index 1579a121b6..4b74b4991e 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs @@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Taiko.Mods public class TaikoModAutoplay : ModAutoplay { public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) - => new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "mekkadosu!" }); + => new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" }); } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs index 831e182193..fee0cb2744 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs @@ -12,6 +12,6 @@ namespace osu.Game.Rulesets.Taiko.Mods public class TaikoModCinema : ModCinema { public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) - => new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedReplayUser { Username = "mekkadosu!" }); + => new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" }); } } diff --git a/osu.Game/Rulesets/Mods/ICreateReplay.cs b/osu.Game/Rulesets/Mods/ICreateReplay.cs index 4883ce5842..1e5eeca92c 100644 --- a/osu.Game/Rulesets/Mods/ICreateReplay.cs +++ b/osu.Game/Rulesets/Mods/ICreateReplay.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mods ModReplayData ICreateReplayData.CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) { var replayScore = CreateReplayScore(beatmap, mods); - return new ModReplayData(replayScore.Replay, new ModCreatedReplayUser { Username = replayScore.ScoreInfo.User.Username }); + return new ModReplayData(replayScore.Replay, new ModCreatedUser { Username = replayScore.ScoreInfo.User.Username }); } } } diff --git a/osu.Game/Rulesets/Mods/ICreateReplayData.cs b/osu.Game/Rulesets/Mods/ICreateReplayData.cs index 2b8305ac78..7d208e9000 100644 --- a/osu.Game/Rulesets/Mods/ICreateReplayData.cs +++ b/osu.Game/Rulesets/Mods/ICreateReplayData.cs @@ -41,19 +41,19 @@ namespace osu.Game.Rulesets.Mods /// /// Placeholder user data to show in place of the local user when the associated mod is active. /// - public readonly ModCreatedReplayUser User; + public readonly ModCreatedUser User; - public ModReplayData(Replay replay, ModCreatedReplayUser user = null) + public ModReplayData(Replay replay, ModCreatedUser user = null) { Replay = replay; - User = user ?? new ModCreatedReplayUser(); + User = user ?? new ModCreatedUser(); } } /// /// A user which is associated with a replay that was created by a mod (ie. autoplay or cinema). /// - public class ModCreatedReplayUser : IUser + public class ModCreatedUser : IUser { public int OnlineID => APIUser.SYSTEM_USER_ID; public bool IsBot => true; diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 7e54c170bf..7a68de49b3 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mods var replayScore = CreateReplayScore(beatmap, mods); #pragma warning restore CS0618 - return new ModReplayData(replayScore.Replay, new ModCreatedReplayUser { Username = replayScore.ScoreInfo.User.Username }); + return new ModReplayData(replayScore.Replay, new ModCreatedUser { Username = replayScore.ScoreInfo.User.Username }); } } } From e0d434b89fc51a91301cc0bbb953b4457b8386cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 31 Mar 2022 11:34:32 +0900 Subject: [PATCH 188/285] Remove unused using statement --- osu.Game/Rulesets/Mods/ModAutoplay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 7a68de49b3..87dc627b19 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics.Sprites; -using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Replays; From 6e94a9780bbe26d5a182fed29d7908f34c21baba Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 31 Mar 2022 12:22:56 +0900 Subject: [PATCH 189/285] Remove ScoreInfo allocations in multi leaderboard --- .../Play/HUD/MultiplayerGameplayLeaderboard.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 71998622ef..4832ee04af 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -228,6 +228,7 @@ namespace osu.Game.Screens.Play.HUD public int? Team => (User.MatchState as TeamVersusUserState)?.TeamID; private readonly RulesetInfo ruleset; + private readonly ScoreInfo scoreInfo; public TrackedUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) { @@ -236,6 +237,8 @@ namespace osu.Game.Screens.Play.HUD User = user; ScoreProcessor = scoreProcessor; + scoreInfo = new ScoreInfo { Ruleset = ruleset }; + ScoringMode.BindValueChanged(_ => UpdateScore()); } @@ -253,12 +256,10 @@ namespace osu.Game.Screens.Play.HUD { var header = frame.Header; - Score.Value = ScoreProcessor.ComputePartialScore(ScoringMode.Value, new ScoreInfo - { - Ruleset = ruleset, - MaxCombo = header.MaxCombo, - Statistics = header.Statistics - }); + scoreInfo.MaxCombo = header.MaxCombo; + scoreInfo.Statistics = header.Statistics; + + Score.Value = ScoreProcessor.ComputePartialScore(ScoringMode.Value, scoreInfo); Accuracy.Value = header.Accuracy; CurrentCombo.Value = header.Combo; From 9c19ae1df8061373e8a2f9481d17885def75d691 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 31 Mar 2022 06:40:38 +0300 Subject: [PATCH 190/285] Remove no longer necessary ruleset field --- osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 4832ee04af..4f5edab526 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -227,13 +227,10 @@ namespace osu.Game.Screens.Play.HUD public int? Team => (User.MatchState as TeamVersusUserState)?.TeamID; - private readonly RulesetInfo ruleset; private readonly ScoreInfo scoreInfo; public TrackedUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) { - this.ruleset = ruleset; - User = user; ScoreProcessor = scoreProcessor; From 36c02573a8e137a5e24d622b6f50893eade5b740 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 30 Mar 2022 21:26:30 -0700 Subject: [PATCH 191/285] Fix osu! logo being clicked when exiting via cmd-q on initial state --- osu.Game/Screens/Menu/ButtonSystem.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index a1f0d22efc..8eeb90a3fd 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -207,6 +207,9 @@ namespace osu.Game.Screens.Menu protected override bool OnKeyDown(KeyDownEvent e) { + if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed) + return false; + if (State == ButtonSystemState.Initial) { if (buttonsTopLevel.Any(b => e.Key == b.TriggerKey)) From 615e4f0f3b3c7a8872c27b70017a4ad385697780 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 28 Mar 2022 15:16:37 -0700 Subject: [PATCH 192/285] Ignore line ending normalisation commit from blame --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..8be6479043 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Normalize all the line endings +32a74f95a5c80a0ed18e693f13a47522099df5c3 From 40b6f3ff0a1f21da5f2b98b7ed6398906e8500c0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 31 Mar 2022 15:09:06 +0900 Subject: [PATCH 193/285] Rename method to CalculateAllLegacyCombinations() --- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 0935f26de6..b5aec0d659 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -121,10 +121,10 @@ namespace osu.Game.Rulesets.Difficulty /// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap. /// /// - /// This should only be used to compute difficulties for legacy mod combinations. + /// This can only be used to compute difficulties for legacy mod combinations. /// /// A collection of structures describing the difficulty of the beatmap for each mod combination. - public IEnumerable CalculateAll(CancellationToken cancellationToken = default) + public IEnumerable CalculateAllLegacyCombinations(CancellationToken cancellationToken = default) { var rulesetInstance = ruleset.CreateInstance(); From e8a295a61bc548ca8878ea2ca92f6aa04ec20d9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 31 Mar 2022 15:18:24 +0900 Subject: [PATCH 194/285] Add test coverage of textbox focus in settings panels when switching between nested panels --- .../Visual/Settings/TestSceneSettingsPanel.cs | 57 ++++++++++++++++++- osu.Game/Overlays/SettingsSubPanel.cs | 2 +- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs index 0af77b3b5a..f9c9b2a68b 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs @@ -1,16 +1,21 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; +using osu.Game.Overlays.Settings.Sections; +using osu.Game.Overlays.Settings.Sections.Input; +using osuTK.Input; namespace osu.Game.Tests.Visual.Settings { [TestFixture] - public class TestSceneSettingsPanel : OsuTestScene + public class TestSceneSettingsPanel : OsuManualInputManagerTestScene { private SettingsPanel settings; private DialogOverlay dialogOverlay; @@ -33,7 +38,55 @@ namespace osu.Game.Tests.Visual.Settings public void ToggleVisibility() { AddWaitStep("wait some", 5); - AddToggleStep("toggle editor visibility", visible => settings.ToggleVisibility()); + AddToggleStep("toggle visibility", visible => settings.ToggleVisibility()); + } + + [Test] + public void TestTextboxFocusAfterNestedPanelBackButton() + { + AddUntilStep("sections loaded", () => settings.SectionsContainer.Children.Count > 0); + AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); + + AddStep("open key binding subpanel", () => + { + settings.SectionsContainer + .ChildrenOfType().FirstOrDefault()? + .ChildrenOfType().FirstOrDefault()? + .TriggerClick(); + }); + + AddUntilStep("binding panel textbox focused", () => settings + .ChildrenOfType().FirstOrDefault()? + .ChildrenOfType().FirstOrDefault()?.HasFocus == true); + + AddStep("Press back", () => settings + .ChildrenOfType().FirstOrDefault()? + .ChildrenOfType().FirstOrDefault()?.TriggerClick()); + + AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); + } + + [Test] + public void TestTextboxFocusAfterNestedPanelEscape() + { + AddUntilStep("sections loaded", () => settings.SectionsContainer.Children.Count > 0); + AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); + + AddStep("open key binding subpanel", () => + { + settings.SectionsContainer + .ChildrenOfType().FirstOrDefault()? + .ChildrenOfType().FirstOrDefault()? + .TriggerClick(); + }); + + AddUntilStep("binding panel textbox focused", () => settings + .ChildrenOfType().FirstOrDefault()? + .ChildrenOfType().FirstOrDefault()?.HasFocus == true); + + AddStep("Escape", () => InputManager.Key(Key.Escape)); + + AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs index da806c09d3..d55c609d3f 100644 --- a/osu.Game/Overlays/SettingsSubPanel.cs +++ b/osu.Game/Overlays/SettingsSubPanel.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays protected override bool DimMainContent => false; // dimming is handled by main overlay - private class BackButton : SidebarButton + public class BackButton : SidebarButton { private Container content; From 726b49fdf3e170d67a9d07e54de12e1df0c741f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 31 Mar 2022 15:19:34 +0900 Subject: [PATCH 195/285] Focus focus not being transferred correctly to parent settings panel on exiting nested panel Seemingly harmless schedule delay ommission meant that the textbox may not be in a state it can handle the incoming focus event. Regressed in https://github.com/ppy/osu/pull/14345#discussion_r690697501. --- osu.Game/Graphics/UserInterface/FocusedTextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index 578ff3c618..d3a76a0f1a 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -28,7 +28,7 @@ namespace osu.Game.Graphics.UserInterface if (!allowImmediateFocus) return; - Scheduler.Add(() => GetContainingInputManager().ChangeFocus(this), false); + Scheduler.Add(() => GetContainingInputManager().ChangeFocus(this)); } public new void KillFocus() => base.KillFocus(); From ef5e37c47a1099c393238d53e8e80f9621c1b06a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 31 Mar 2022 16:18:26 +0900 Subject: [PATCH 196/285] Fix multiplayer sounds playing too much after gameplay Regressed in https://github.com/ppy/osu/pull/15936. Closes #17295. --- .../OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs index d467a32acb..49b5b7fed9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs @@ -37,21 +37,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.UserJoined(user); - userJoinedSample?.Play(); + Scheduler.AddOnce(() => userJoinedSample?.Play()); } protected override void UserLeft(MultiplayerRoomUser user) { base.UserLeft(user); - userLeftSample?.Play(); + Scheduler.AddOnce(() => userLeftSample?.Play()); } protected override void UserKicked(MultiplayerRoomUser user) { base.UserKicked(user); - userKickedSample?.Play(); + Scheduler.AddOnce(() => userKickedSample?.Play()); } private void hostChanged(ValueChangedEvent value) @@ -59,7 +59,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // only play sound when the host changes from an already-existing host. if (value.OldValue == null) return; - hostChangedSample?.Play(); + Scheduler.AddOnce(() => hostChangedSample?.Play()); } } } From a7a7584d3ec5077c98fa7c8bb02aa7beb5bda09e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 31 Mar 2022 18:31:18 +0900 Subject: [PATCH 197/285] Add test coverage ensuring ruleset ID is correct after bracket read Historically, tournament client may have written incorrect `OnlineID` values. We wanted to use `ShortName` to re-fetch the ruleset. This test ensures this flow is working correctly. --- .../NonVisual/DataLoadTest.cs | 60 ++++++++++++++++++- osu.Game.Tournament/TournamentGameBase.cs | 4 +- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament.Tests/NonVisual/DataLoadTest.cs b/osu.Game.Tournament.Tests/NonVisual/DataLoadTest.cs index 65753bfe00..4c1256df2e 100644 --- a/osu.Game.Tournament.Tests/NonVisual/DataLoadTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/DataLoadTest.cs @@ -1,9 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.IO; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Game.Rulesets; using osu.Game.Tests; @@ -12,6 +16,45 @@ namespace osu.Game.Tournament.Tests.NonVisual { public class DataLoadTest : TournamentHostTest { + [Test] + public void TestRulesetGetsValidOnlineID() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = new TestTournament(runOnLoadComplete: () => + { + // ReSharper disable once AccessToDisposedClosure + var storage = host.Storage.GetStorageForDirectory(Path.Combine("tournaments", "default")); + + using (var stream = storage.GetStream("bracket.json", FileAccess.Write, FileMode.Create)) + using (var writer = new StreamWriter(stream)) + { + writer.Write(@"{ + ""Ruleset"": { + ""ShortName"": ""taiko"", + ""OnlineID"": -1, + ""Name"": ""osu!taiko"", + ""InstantiationInfo"": ""osu.Game.Rulesets.OsuTaiko.TaikoRuleset, osu.Game.Rulesets.Taiko"", + ""Available"": true + } }"); + } + }); + + LoadTournament(host, osu); + + osu.BracketLoadTask.WaitSafely(); + + Assert.That(osu.Dependencies.Get>().Value.OnlineID, Is.EqualTo(1)); + } + finally + { + host.Exit(); + } + } + } + [Test] public void TestUnavailableRuleset() { @@ -19,7 +62,7 @@ namespace osu.Game.Tournament.Tests.NonVisual { try { - var osu = new TestTournament(); + var osu = new TestTournament(true); LoadTournament(host, osu); var storage = osu.Dependencies.Get(); @@ -35,10 +78,23 @@ namespace osu.Game.Tournament.Tests.NonVisual public class TestTournament : TournamentGameBase { + private readonly bool resetRuleset; + private readonly Action runOnLoadComplete; + + public new Task BracketLoadTask => base.BracketLoadTask; + + public TestTournament(bool resetRuleset = false, Action runOnLoadComplete = null) + { + this.resetRuleset = resetRuleset; + this.runOnLoadComplete = runOnLoadComplete; + } + protected override void LoadComplete() { + runOnLoadComplete?.Invoke(); base.LoadComplete(); - Ruleset.Value = new RulesetInfo(); // not available + if (resetRuleset) + Ruleset.Value = new RulesetInfo(); // not available } } } diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index f318c8bd85..b88f4edaa6 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -64,8 +64,6 @@ namespace osu.Game.Tournament Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage))); dependencies.CacheAs(new StableInfo(storage)); - - Task.Run(readBracket); } private void readBracket() @@ -290,6 +288,8 @@ namespace osu.Game.Tournament MenuCursorContainer.Cursor.Alpha = 0; base.LoadComplete(); + + Task.Run(readBracket); } protected virtual void SaveChanges() From a06b0a49662c94b53c05d1c482d6457cf1c3bb98 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 31 Mar 2022 18:40:58 +0900 Subject: [PATCH 198/285] Fix tournament bracket parsing's ruleset refetch logic not working correctly Due to equality being based on `ShortName`, it was feasible that the re-fetch exited early (in bindable shortcutting logic) causing the ruleset's `OnlineID` to remain `-1` or something equally wrong. Resolves issue pointed out at https://github.com/ppy/osu/discussions/17538#discussioncomment-2471746. --- osu.Game.Tournament/TournamentGameBase.cs | 30 +++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index b88f4edaa6..a251a043f7 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -66,6 +66,18 @@ namespace osu.Game.Tournament dependencies.CacheAs(new StableInfo(storage)); } + protected override void LoadComplete() + { + MenuCursorContainer.Cursor.AlwaysPresent = true; // required for tooltip display + + // we don't want to show the menu cursor as it would appear on stream output. + MenuCursorContainer.Cursor.Alpha = 0; + + base.LoadComplete(); + + Task.Run(readBracket); + } + private void readBracket() { try @@ -79,10 +91,14 @@ namespace osu.Game.Tournament ladder ??= new LadderInfo(); - ladder.Ruleset.Value = ladder.Ruleset.Value != null + var resolvedRuleset = ladder.Ruleset.Value != null ? RulesetStore.GetRuleset(ladder.Ruleset.Value.ShortName) : RulesetStore.AvailableRulesets.First(); + // Must set to null initially to avoid the following re-fetch hitting `ShortName` based equality check. + ladder.Ruleset.Value = null; + ladder.Ruleset.Value = resolvedRuleset; + bool addedInfo = false; // assign teams @@ -280,18 +296,6 @@ namespace osu.Game.Tournament } } - protected override void LoadComplete() - { - MenuCursorContainer.Cursor.AlwaysPresent = true; // required for tooltip display - - // we don't want to show the menu cursor as it would appear on stream output. - MenuCursorContainer.Cursor.Alpha = 0; - - base.LoadComplete(); - - Task.Run(readBracket); - } - protected virtual void SaveChanges() { if (!bracketLoadTaskCompletionSource.Task.IsCompletedSuccessfully) From 0a34ce2509d1df942034704a9baa057010b1af47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 31 Mar 2022 19:07:17 +0900 Subject: [PATCH 199/285] Increase font weight for runtime clock Fonts this small are required to be `SemiBold` by design guidelines. Somehow missed this. --- osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs b/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs index 090f8c4a0f..81a362450c 100644 --- a/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs +++ b/osu.Game/Overlays/Toolbar/DigitalClockDisplay.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osuTK; namespace osu.Game.Overlays.Toolbar { @@ -42,7 +41,7 @@ namespace osu.Game.Overlays.Toolbar { Y = 14, Colour = colours.PinkLight, - Scale = new Vector2(0.6f) + Font = OsuFont.Default.With(size: 10, weight: FontWeight.SemiBold), } }; From f1aa60c0f1c2fec417db5bc05d8d6dd2eb3dd391 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Thu, 31 Mar 2022 16:26:53 +0200 Subject: [PATCH 200/285] Make the clock feel more like a button --- osu.Game/Overlays/Toolbar/ToolbarClock.cs | 92 +++++++++++++++++------ 1 file changed, 68 insertions(+), 24 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs index ad5c9ac7a1..02230c13cf 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -3,54 +3,83 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osuTK; +using osuTK.Graphics; + namespace osu.Game.Overlays.Toolbar { - public class ToolbarClock : CompositeDrawable + public class ToolbarClock : OsuClickableContainer { private Bindable clockDisplayMode; + protected Box HoverBackground; + private readonly Box flashBackground; + private readonly FillFlowContainer clockContainer; + private DigitalClockDisplay digital; private AnalogClockDisplay analog; public ToolbarClock() + : base(HoverSampleSet.Toolbar) { RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; - Padding = new MarginPadding(10); + Children = new Drawable[] + { + HoverBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(80).Opacity(180), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + flashBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Colour = Color4.White.Opacity(100), + Blending = BlendingParameters.Additive, + }, + + clockContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Padding = new MarginPadding(10), + Children = new Drawable[] + { + analog = new AnalogClockDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + digital = new DigitalClockDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + } + }; } [BackgroundDependencyLoader] private void load(OsuConfigManager config) { clockDisplayMode = config.GetBindable(OsuSetting.ToolbarClockDisplayMode); - - InternalChild = new FillFlowContainer - { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Children = new Drawable[] - { - analog = new AnalogClockDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - digital = new DigitalClockDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - } - }; } protected override void LoadComplete() @@ -72,8 +101,23 @@ namespace osu.Game.Overlays.Toolbar protected override bool OnClick(ClickEvent e) { + flashBackground.FadeOutFromOne(800, Easing.OutQuint); + cycleDisplayMode(); - return true; + + return base.OnClick(e); + } + + protected override bool OnHover(HoverEvent e) + { + HoverBackground.FadeIn(200); + + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + HoverBackground.FadeOut(200); } private void cycleDisplayMode() From c64a90b39e86a38b7ce5a54ff950ee8f7b986321 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Thu, 31 Mar 2022 17:21:50 +0200 Subject: [PATCH 201/285] Remove a newline to comply with codefactor --- osu.Game/Overlays/Toolbar/ToolbarClock.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs index 02230c13cf..48de803b3a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -15,7 +15,6 @@ using osu.Game.Graphics.UserInterface; using osuTK; using osuTK.Graphics; - namespace osu.Game.Overlays.Toolbar { public class ToolbarClock : OsuClickableContainer From 52d723aaa6ee71876054fcb240f8f2bcc182338a Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Thu, 31 Mar 2022 20:11:07 +0200 Subject: [PATCH 202/285] Remove BPM slider --- osu.Game/Screens/Edit/Timing/TimingSection.cs | 50 ------------------- 1 file changed, 50 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index cd0b56d338..f3c468de4c 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -13,7 +13,6 @@ namespace osu.Game.Screens.Edit.Timing { internal class TimingSection : Section { - private SettingsSlider bpmSlider; private LabelledTimeSignature timeSignature; private BPMTextBox bpmTextEntry; @@ -23,7 +22,6 @@ namespace osu.Game.Screens.Edit.Timing Flow.AddRange(new Drawable[] { bpmTextEntry = new BPMTextBox(), - bpmSlider = new BPMSlider(), timeSignature = new LabelledTimeSignature { Label = "Time Signature" @@ -35,9 +33,6 @@ namespace osu.Game.Screens.Edit.Timing { if (point.NewValue != null) { - bpmSlider.Current = point.NewValue.BeatLengthBindable; - bpmSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); - bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable; // no need to hook change handler here as it's the same bindable as above @@ -102,51 +97,6 @@ namespace osu.Game.Screens.Edit.Timing } } - private class BPMSlider : SettingsSlider - { - private const double sane_minimum = 60; - private const double sane_maximum = 240; - - private readonly BindableNumber beatLengthBindable = new TimingControlPoint().BeatLengthBindable; - - private readonly BindableDouble bpmBindable = new BindableDouble(60000 / TimingControlPoint.DEFAULT_BEAT_LENGTH) - { - MinValue = sane_minimum, - MaxValue = sane_maximum, - }; - - public BPMSlider() - { - beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue)), true); - bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); - - base.Current = bpmBindable; - - TransferValueOnCommit = true; - } - - public override Bindable Current - { - get => base.Current; - set - { - // incoming will be beat length, not bpm - beatLengthBindable.UnbindBindings(); - beatLengthBindable.BindTo(value); - } - } - - private void updateCurrent(double newValue) - { - // we use a more sane range for the slider display unless overridden by the user. - // if a value comes in outside our range, we should expand temporarily. - bpmBindable.MinValue = Math.Min(newValue, sane_minimum); - bpmBindable.MaxValue = Math.Max(newValue, sane_maximum); - - bpmBindable.Value = newValue; - } - } - private static double beatLengthToBpm(double beatLength) => 60000 / beatLength; } } From bdb21b17f721ab0a9b9f302439cd62033eafae14 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Thu, 31 Mar 2022 20:39:26 +0200 Subject: [PATCH 203/285] Fix my code according to the changes @bdach requested --- osu.Game/Overlays/Toolbar/ToolbarClock.cs | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs index 48de803b3a..22a96603dc 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs @@ -21,9 +21,8 @@ namespace osu.Game.Overlays.Toolbar { private Bindable clockDisplayMode; - protected Box HoverBackground; - private readonly Box flashBackground; - private readonly FillFlowContainer clockContainer; + private Box hoverBackground; + private Box flashBackground; private DigitalClockDisplay digital; private AnalogClockDisplay analog; @@ -33,10 +32,16 @@ namespace osu.Game.Overlays.Toolbar { RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + clockDisplayMode = config.GetBindable(OsuSetting.ToolbarClockDisplayMode); Children = new Drawable[] { - HoverBackground = new Box + hoverBackground = new Box { RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(80).Opacity(180), @@ -50,8 +55,7 @@ namespace osu.Game.Overlays.Toolbar Colour = Color4.White.Opacity(100), Blending = BlendingParameters.Additive, }, - - clockContainer = new FillFlowContainer + new FillFlowContainer { RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, @@ -75,12 +79,6 @@ namespace osu.Game.Overlays.Toolbar }; } - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - clockDisplayMode = config.GetBindable(OsuSetting.ToolbarClockDisplayMode); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -109,14 +107,16 @@ namespace osu.Game.Overlays.Toolbar protected override bool OnHover(HoverEvent e) { - HoverBackground.FadeIn(200); + hoverBackground.FadeIn(200); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - HoverBackground.FadeOut(200); + hoverBackground.FadeOut(200); + + base.OnHoverLost(e); } private void cycleDisplayMode() From a6875383fcd5da6d50304bc40a660f35112e3f3f Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Thu, 31 Mar 2022 21:06:05 +0200 Subject: [PATCH 204/285] Rebind `SaveState()` to bpmTextEntry --- osu.Game/Screens/Edit/Timing/TimingSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index f3c468de4c..07f8f2ba3a 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Timing if (point.NewValue != null) { bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable; - // no need to hook change handler here as it's the same bindable as above + bpmTextEntry.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); timeSignature.Current = point.NewValue.TimeSignatureBindable; timeSignature.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); From e14d5b8adbc26bff6b9f9f184a9a9444ba18bb8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 31 Mar 2022 21:20:30 +0200 Subject: [PATCH 205/285] Remove unused using directives --- osu.Game/Screens/Edit/Timing/TimingSection.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 07f8f2ba3a..13af04cd4b 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -1,13 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Overlays.Settings; namespace osu.Game.Screens.Edit.Timing { From b3896257ca0721e50ce5e21f8572ade3da40dccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 31 Mar 2022 22:09:03 +0200 Subject: [PATCH 206/285] Move shear amount to constant --- .../Visual/UserInterface/TestSceneShearedToggleButton.cs | 4 ++-- osu.Game/Graphics/UserInterface/ShearedToggleButton.cs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs index a969858157..5082e93f37 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("create button", () => { - Child = button = new ShearedToggleButton(0.2f) + Child = button = new ShearedToggleButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("create button", () => { - Child = button = new ShearedToggleButton(0.2f) + Child = button = new ShearedToggleButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs index acbed29279..27d2611983 100644 --- a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs @@ -37,10 +37,12 @@ namespace osu.Game.Graphics.UserInterface private Sample? sampleOff; private Sample? sampleOn; + private const float shear = 0.2f; + [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public ShearedToggleButton(float shear) + public ShearedToggleButton() { Height = 50; Padding = new MarginPadding { Horizontal = shear * 50 }; From e180db145dcee071d8e559345a1bdb25f66856ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 31 Mar 2022 22:19:08 +0200 Subject: [PATCH 207/285] Add constructor argument to facilitate fixed width/autosizing --- .../TestSceneShearedToggleButton.cs | 25 +++++++++++++++++-- .../UserInterface/ShearedToggleButton.cs | 22 +++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs index 5082e93f37..f12cbc4979 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs @@ -23,12 +23,11 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("create button", () => { - Child = button = new ShearedToggleButton + Child = button = new ShearedToggleButton(200) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "Toggle me", - Width = 200 }; }); @@ -36,6 +35,28 @@ namespace osu.Game.Tests.Visual.UserInterface AddToggleStep("toggle disabled", disabled => button.Active.Disabled = disabled); } + [Test] + public void TestSizing() + { + ShearedToggleButton toggleButton = null; + + AddStep("create fixed width button", () => Child = toggleButton = new ShearedToggleButton(200) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Fixed width" + }); + AddStep("change text", () => toggleButton.Text = "New text"); + + AddStep("create auto-sizing button", () => Child = toggleButton = new ShearedToggleButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "This button autosizes to its text!" + }); + AddStep("change text", () => toggleButton.Text = "New text"); + } + [Test] public void TestDisabledState() { diff --git a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs index 27d2611983..aed3be20a0 100644 --- a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs @@ -42,7 +42,17 @@ namespace osu.Game.Graphics.UserInterface [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public ShearedToggleButton() + /// + /// Creates a new + /// + /// + /// The width of the button. + /// + /// If a non- value is provided, this button will have a fixed width equal to the provided value. + /// If a value is provided (or the argument is omitted entirely), the button will autosize in width to fit the text. + /// + /// + public ShearedToggleButton(float? width = null) { Height = 50; Padding = new MarginPadding { Horizontal = shear * 50 }; @@ -67,6 +77,16 @@ namespace osu.Game.Graphics.UserInterface Shear = new Vector2(-shear, 0) } }; + + if (width != null) + { + Width = width.Value; + } + else + { + AutoSizeAxes = Axes.X; + text.Margin = new MarginPadding { Horizontal = 15 }; + } } [BackgroundDependencyLoader] From 058350dfd8bf7c7754ccc25bb20357185a750817 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Apr 2022 10:53:14 +0900 Subject: [PATCH 208/285] Fix failing test due to incorrect sizing specification --- .../Visual/UserInterface/TestSceneShearedToggleButton.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs index f12cbc4979..b5109aa58d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedToggleButton.cs @@ -64,12 +64,11 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("create button", () => { - Child = button = new ShearedToggleButton + Child = button = new ShearedToggleButton(200) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "Toggle me", - Width = 200 }; }); From a987cda30daa1a235b035fafeb4add95618ddcc8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Apr 2022 12:15:26 +0900 Subject: [PATCH 209/285] Rename "Aim Assist" to "Magnetised" to better suit the mod's behaviour As proposed in https://github.com/ppy/osu/discussions/17375. --- ...uModAimAssist.cs => TestSceneOsuModMagnetised.cs} | 6 +++--- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs | 2 +- .../Mods/{OsuModAimAssist.cs => OsuModMagnetised.cs} | 12 ++++++------ osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) rename osu.Game.Rulesets.Osu.Tests/Mods/{TestSceneOsuModAimAssist.cs => TestSceneOsuModMagnetised.cs} (79%) rename osu.Game.Rulesets.Osu/Mods/{OsuModAimAssist.cs => OsuModMagnetised.cs} (87%) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMagnetised.cs similarity index 79% rename from osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs rename to osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMagnetised.cs index b8310bc4e7..e7a40d6337 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMagnetised.cs @@ -6,16 +6,16 @@ using osu.Game.Rulesets.Osu.Mods; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public class TestSceneOsuModAimAssist : OsuModTestScene + public class TestSceneOsuModMagnetised : OsuModTestScene { [TestCase(0.1f)] [TestCase(0.5f)] [TestCase(1)] - public void TestAimAssist(float strength) + public void TestMagnetised(float strength) { CreateModTest(new ModTestData { - Mod = new OsuModAimAssist + Mod = new OsuModMagnetised { AssistStrength = { Value = strength }, }, diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 983964d639..aaf455e95f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Automation; public override string Description => @"Automatic cursor movement - just follow the rhythm."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModAimAssist) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised) }; public bool PerformFail() => false; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs index 31179cdf4a..b31ef5d2fd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModAutoplay : ModAutoplay { - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs index d677ab43d0..5b42772358 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModCinema : ModCinema { - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs similarity index 87% rename from osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs rename to osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index 1abbd67d8f..31598c50e7 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAimAssist.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -16,19 +16,19 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Mods { - internal class OsuModAimAssist : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset + internal class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset { - public override string Name => "Aim Assist"; - public override string Acronym => "AA"; - public override IconUsage? Icon => FontAwesome.Solid.MousePointer; + public override string Name => "Magnetised"; + public override string Acronym => "MG"; + public override IconUsage? Icon => FontAwesome.Solid.Magnet; public override ModType Type => ModType.Fun; - public override string Description => "No need to chase the circle – the circle chases you!"; + public override string Description => "No need to chase the circles – your cursor is a magnet!"; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) }; private IFrameStableClock gameplayClock; - [SettingSource("Assist strength", "How much this mod will assist you.", 0)] + [SettingSource("Attraction strength", "How strong the pull is.", 0)] public BindableFloat AssistStrength { get; } = new BindableFloat(0.5f) { Precision = 0.05f, diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 9719de441e..6b81efdca6 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer { public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModAimAssist) }).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised) }).ToArray(); /// /// How early before a hitobject's start time to trigger a hit. diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs index 28c3b069b6..45ce4d555a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override string Description => "Everything rotates. EVERYTHING."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModAimAssist) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised) }; private float theta; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs index 40a05400ea..693a5bee0b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override string Description => "They just won't stay still..."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModAimAssist) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised) }; private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles private const int wiggle_strength = 10; // Higher = stronger wiggles diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 47a2618ddd..207e7a4ab0 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Osu new OsuModApproachDifferent(), new OsuModMuted(), new OsuModNoScope(), - new OsuModAimAssist(), + new OsuModMagnetised(), new ModAdaptiveSpeed() }; From ea672745b0d026ce7ca08d45239dca083e614635 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Apr 2022 12:44:49 +0900 Subject: [PATCH 210/285] Add ability to switch between most common tournament scenes using key bindings --- osu.Game.Tournament/TournamentSceneManager.cs | 66 ++++++++++++++++--- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index 80a9c07cde..98338244e4 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -7,8 +7,10 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Tournament.Components; using osu.Game.Tournament.Screens; using osu.Game.Tournament.Screens.Drawings; @@ -23,6 +25,7 @@ using osu.Game.Tournament.Screens.TeamIntro; using osu.Game.Tournament.Screens.TeamWin; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Tournament { @@ -123,16 +126,16 @@ namespace osu.Game.Tournament new ScreenButton(typeof(RoundEditorScreen)) { Text = "Rounds Editor", RequestSelection = SetScreen }, new ScreenButton(typeof(LadderEditorScreen)) { Text = "Bracket Editor", RequestSelection = SetScreen }, new Separator(), - new ScreenButton(typeof(ScheduleScreen)) { Text = "Schedule", RequestSelection = SetScreen }, - new ScreenButton(typeof(LadderScreen)) { Text = "Bracket", RequestSelection = SetScreen }, + new ScreenButton(typeof(ScheduleScreen), Key.S) { Text = "Schedule", RequestSelection = SetScreen }, + new ScreenButton(typeof(LadderScreen), Key.B) { Text = "Bracket", RequestSelection = SetScreen }, new Separator(), - new ScreenButton(typeof(TeamIntroScreen)) { Text = "Team Intro", RequestSelection = SetScreen }, - new ScreenButton(typeof(SeedingScreen)) { Text = "Seeding", RequestSelection = SetScreen }, + new ScreenButton(typeof(TeamIntroScreen), Key.I) { Text = "Team Intro", RequestSelection = SetScreen }, + new ScreenButton(typeof(SeedingScreen), Key.D) { Text = "Seeding", RequestSelection = SetScreen }, new Separator(), - new ScreenButton(typeof(MapPoolScreen)) { Text = "Map Pool", RequestSelection = SetScreen }, - new ScreenButton(typeof(GameplayScreen)) { Text = "Gameplay", RequestSelection = SetScreen }, + new ScreenButton(typeof(MapPoolScreen), Key.M) { Text = "Map Pool", RequestSelection = SetScreen }, + new ScreenButton(typeof(GameplayScreen), Key.G) { Text = "Gameplay", RequestSelection = SetScreen }, new Separator(), - new ScreenButton(typeof(TeamWinScreen)) { Text = "Win", RequestSelection = SetScreen }, + new ScreenButton(typeof(TeamWinScreen), Key.W) { Text = "Win", RequestSelection = SetScreen }, new Separator(), new ScreenButton(typeof(DrawingsScreen)) { Text = "Drawings", RequestSelection = SetScreen }, new ScreenButton(typeof(ShowcaseScreen)) { Text = "Showcase", RequestSelection = SetScreen }, @@ -231,13 +234,60 @@ namespace osu.Game.Tournament { public readonly Type Type; - public ScreenButton(Type type) + private readonly Key? shortcutKey; + + public ScreenButton(Type type, Key? shortcutKey = null) { + this.shortcutKey = shortcutKey; + Type = type; + BackgroundColour = OsuColour.Gray(0.2f); Action = () => RequestSelection?.Invoke(type); RelativeSizeAxes = Axes.X; + + if (shortcutKey != null) + { + Add(new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(24), + Margin = new MarginPadding(5), + Masking = true, + CornerRadius = 4, + Alpha = 0.5f, + Blending = BlendingParameters.Additive, + Children = new Drawable[] + { + new Box + { + Colour = OsuColour.Gray(0.1f), + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Font = OsuFont.Default.With(size: 24), + Y = -2, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = shortcutKey.ToString(), + } + } + }); + } + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Key == shortcutKey) + { + TriggerClick(); + return true; + } + + return base.OnKeyDown(e); } private bool isSelected; From de625125d6d04b97d8571d62267e6e04328dfde8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Apr 2022 13:03:48 +0900 Subject: [PATCH 211/285] Rename magnetised mod attraction strength property to match new naming --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMagnetised.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMagnetised.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMagnetised.cs index e7a40d6337..9b49e60363 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModMagnetised.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { Mod = new OsuModMagnetised { - AssistStrength = { Value = strength }, + AttractionStrength = { Value = strength }, }, PassCondition = () => true, Autoplay = false, diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index 31598c50e7..ca6e9cfb1d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Mods private IFrameStableClock gameplayClock; [SettingSource("Attraction strength", "How strong the pull is.", 0)] - public BindableFloat AssistStrength { get; } = new BindableFloat(0.5f) + public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f) { Precision = 0.05f, MinValue = 0.05f, @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Mods private void easeTo(DrawableHitObject hitObject, Vector2 destination) { - double dampLength = Interpolation.Lerp(3000, 40, AssistStrength.Value); + double dampLength = Interpolation.Lerp(3000, 40, AttractionStrength.Value); float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime); float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime); From 69d4f8612268ab280408f765d44256e385f25f9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Apr 2022 14:11:53 +0900 Subject: [PATCH 212/285] Fix automatically created "(modified)" skins getting conflicting names Applies the already tested and proven method that is used in the editor to the mutable skin creation flow. --- osu.Game/Skinning/SkinManager.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index bad559d9fe..44b9c69794 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -24,6 +24,7 @@ using osu.Game.Database; using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Overlays.Notifications; +using osu.Game.Utils; namespace osu.Game.Skinning { @@ -144,20 +145,26 @@ namespace osu.Game.Skinning if (!s.Protected) return; + var existingSkinNames = realm.Run(r => r.All() + .Where(skin => !skin.DeletePending) + .AsEnumerable() + .Select(skin => skin.Name)); + // if the user is attempting to save one of the default skin implementations, create a copy first. - var result = skinModelManager.Import(new SkinInfo + var skinInfo = new SkinInfo { - Name = s.Name + @" (modified)", Creator = s.Creator, InstantiationInfo = s.InstantiationInfo, - }); + Name = NamingUtils.GetNextBestName(existingSkinNames, $"{s.Name} (modified)") + }; + + var result = skinModelManager.Import(skinInfo); if (result != null) { // save once to ensure the required json content is populated. // currently this only happens on save. result.PerformRead(skin => Save(skin.CreateInstance(this))); - CurrentSkinInfo.Value = result; } }); From 88306a61804c4d960f0c1b06090f9c914fb3f6b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Apr 2022 14:22:26 +0900 Subject: [PATCH 213/285] Disable ability to select random skin from within the skin editor Reasoning is explained in inline comment. I knowingly only applied this to the shortcut key. It's still feasible a user can choose the option from the skin dropdown while the editor is open, but that's less of an issue (because a user won't get the same compulsion that I get to mash the key, only to be greeted with 100 new mutable skins created). --- osu.Game/OsuGame.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 4cd954a646..73121f6e7d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1061,6 +1061,12 @@ namespace osu.Game return true; case GlobalAction.RandomSkin: + // Don't allow random skin selection while in the skin editor. + // This is mainly to stop many "osu! default (modified)" skins being created via the SkinManager.EnsureMutableSkin() path. + // If people want this to work we can potentially avoid selecting default skins when the editor is open, or allow a maximum of one mutable skin somehow. + if (skinEditor.State.Value == Visibility.Visible) + return false; + SkinManager.SelectRandomSkin(); return true; } From 01829cf2d89ebec7056a5e5cb082b8d91f5cd46d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Apr 2022 14:30:02 +0900 Subject: [PATCH 214/285] Move `SkinnableInfo` error handling to lower level Handling was recently added to handle the usage in `Skin.GetDrawableCompoent`, but it turns out this is also required for `DrawableExtensions.ApplySkinnableInfo` which can throw in a similar fashion. Found while working on sprite support for the editor, where this becomes an actual issue (ie. switching to a branch where the new sprite support is not present can cause unexpected crashes). --- osu.Game/Screens/Play/HUD/SkinnableInfo.cs | 15 ++++++++++++--- osu.Game/Skinning/Skin.cs | 11 +---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index 95395f8181..1f659fd5bf 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -9,6 +9,7 @@ using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Skinning; @@ -84,9 +85,17 @@ namespace osu.Game.Screens.Play.HUD /// The new instance. public Drawable CreateInstance() { - Drawable d = (Drawable)Activator.CreateInstance(Type); - d.ApplySkinnableInfo(this); - return d; + try + { + Drawable d = (Drawable)Activator.CreateInstance(Type); + d.ApplySkinnableInfo(this); + return d; + } + catch (Exception e) + { + Logger.Error(e, $"Unable to create skin component {Type.Name}"); + return Drawable.Empty(); + } } } } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 2f01bb7301..5d4afc00c4 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -155,16 +155,7 @@ namespace osu.Game.Skinning var components = new List(); foreach (var i in skinnableInfo) - { - try - { - components.Add(i.CreateInstance()); - } - catch (Exception e) - { - Logger.Error(e, $"Unable to create skin component {i.Type.Name}"); - } - } + components.Add(i.CreateInstance()); return new SkinnableTargetComponentsContainer { From 3a16483214bb564dbf75f5bd61f6c7623e0783b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 15:10:26 +0900 Subject: [PATCH 215/285] Add prioritised user lookups for default skin This allows user resources to be consumed before falling back to the game bundled assets. --- osu.Game/Skinning/DefaultSkin.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 7c6d138f4c..43ada59bcb 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -46,13 +46,13 @@ namespace osu.Game.Skinning this.resources = resources; } - public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; + public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Textures?.Get(componentName, wrapModeS, wrapModeT); public override ISample GetSample(ISampleInfo sampleInfo) { foreach (string lookup in sampleInfo.LookupNames) { - var sample = resources.AudioManager.Samples.Get(lookup); + var sample = Samples?.Get(lookup) ?? resources.AudioManager.Samples.Get(lookup); if (sample != null) return sample; } From fca9faac9b87eb9c24d86e2d52764619bbe2ddf0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Mar 2022 15:11:35 +0900 Subject: [PATCH 216/285] Add `SkinnableSprite` for arbitrary sprite additions --- .../Skinning/Components/SkinnableSprite.cs | 49 ++++++++++++++++ osu.Game/Skinning/Editor/SkinEditor.cs | 56 +++++++++++++++++-- osu.Game/Skinning/SkinManager.cs | 44 ++++++++++++++- osu.Game/Skinning/SkinnableSprite.cs | 4 +- 4 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 osu.Game/Skinning/Components/SkinnableSprite.cs diff --git a/osu.Game/Skinning/Components/SkinnableSprite.cs b/osu.Game/Skinning/Components/SkinnableSprite.cs new file mode 100644 index 0000000000..292bbe2321 --- /dev/null +++ b/osu.Game/Skinning/Components/SkinnableSprite.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; + +namespace osu.Game.Skinning.Components +{ + /// + /// Intended to be a test bed for skinning. May be removed at some point in the future. + /// + [UsedImplicitly] + public class SkinSprite : CompositeDrawable, ISkinnableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [SettingSource("Sprite name", "The filename of the sprite")] + public Bindable SpriteName { get; } = new Bindable(string.Empty); + + [Resolved] + private ISkinSource source { get; set; } + + public SkinSprite() + { + AutoSizeAxes = Axes.Both; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + SpriteName.BindValueChanged(spriteName => + { + InternalChildren = new Drawable[] + { + new Sprite + { + Texture = source.GetTexture(SpriteName.Value), + } + }; + }, true); + } + } +} diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 7bf4e94662..7701fafbfc 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -11,6 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Testing; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; @@ -18,11 +21,12 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; +using osu.Game.Skinning.Components; namespace osu.Game.Skinning.Editor { [Cached(typeof(SkinEditor))] - public class SkinEditor : VisibilityContainer + public class SkinEditor : VisibilityContainer, ICanAcceptFiles { public const double TRANSITION_DURATION = 500; @@ -36,6 +40,9 @@ namespace osu.Game.Skinning.Editor private Bindable currentSkin; + [Resolved(canBeNull: true)] + private OsuGame game { get; set; } + [Resolved] private SkinManager skins { get; set; } @@ -171,6 +178,8 @@ namespace osu.Game.Skinning.Editor Show(); + game?.RegisterImportHandler(this); + // as long as the skin editor is loaded, let's make sure we can modify the current skin. currentSkin = skins.CurrentSkin.GetBoundCopy(); @@ -186,6 +195,13 @@ namespace osu.Game.Skinning.Editor SelectedComponents.BindCollectionChanged((_, __) => Scheduler.AddOnce(populateSettings), true); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + game?.UnregisterImportHandler(this); + } + public void UpdateTargetScreen(Drawable targetScreen) { this.targetScreen = targetScreen; @@ -229,15 +245,20 @@ namespace osu.Game.Skinning.Editor } private void placeComponent(Type type) + { + if (!(Activator.CreateInstance(type) is ISkinnableDrawable component)) + throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}."); + + placeComponent(component); + } + + private void placeComponent(ISkinnableDrawable component) { var targetContainer = getFirstTarget(); if (targetContainer == null) return; - if (!(Activator.CreateInstance(type) is ISkinnableDrawable component)) - throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}."); - var drawableComponent = (Drawable)component; // give newly added components a sane starting location. @@ -313,5 +334,32 @@ namespace osu.Game.Skinning.Editor foreach (var item in items) availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item); } + + public Task Import(params string[] paths) + { + Schedule(() => + { + var file = new FileInfo(paths.First()); + + // import to skin + currentSkin.Value.SkinInfo.PerformWrite(skinInfo => + { + using (var contents = file.OpenRead()) + skins.AddFile(skinInfo, contents, file.Name); + }); + + // place component + placeComponent(new SkinSprite + { + SpriteName = { Value = Path.GetFileNameWithoutExtension(file.Name) } + }); + }); + + return Task.CompletedTask; + } + + public Task Import(params ImportTask[] tasks) => throw new NotImplementedException(); + + public IEnumerable HandledExtensions => new[] { ".jpg", ".jpeg", ".png" }; } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index bad559d9fe..5333b58625 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Linq.Expressions; using System.Threading; @@ -23,6 +24,7 @@ using osu.Game.Audio; using osu.Game.Database; using osu.Game.IO; using osu.Game.IO.Archives; +using osu.Game.Models; using osu.Game.Overlays.Notifications; namespace osu.Game.Skinning @@ -35,7 +37,7 @@ namespace osu.Game.Skinning /// For gameplay components, see which adds extra legacy and toggle logic that may affect the lookup process. /// [ExcludeFromDynamicCompile] - public class SkinManager : ISkinSource, IStorageResourceProvider, IModelImporter + public class SkinManager : ISkinSource, IStorageResourceProvider, IModelImporter, IModelManager, IModelFileManager { private readonly AudioManager audio; @@ -306,5 +308,45 @@ namespace osu.Game.Skinning } #endregion + + public bool Delete(SkinInfo item) + { + return skinModelManager.Delete(item); + } + + public void Delete(List items, bool silent = false) + { + skinModelManager.Delete(items, silent); + } + + public void Undelete(List items, bool silent = false) + { + skinModelManager.Undelete(items, silent); + } + + public void Undelete(SkinInfo item) + { + skinModelManager.Undelete(item); + } + + public bool IsAvailableLocally(SkinInfo model) + { + return skinModelManager.IsAvailableLocally(model); + } + + public void ReplaceFile(SkinInfo model, RealmNamedFileUsage file, Stream contents) + { + skinModelManager.ReplaceFile(model, file, contents); + } + + public void DeleteFile(SkinInfo model, RealmNamedFileUsage file) + { + skinModelManager.DeleteFile(model, file); + } + + public void AddFile(SkinInfo model, Stream contents, string filename) + { + skinModelManager.AddFile(model, contents, filename); + } } } diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index 56e576d081..38803bd8e3 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -11,7 +11,7 @@ namespace osu.Game.Skinning /// /// A skinnable element which uses a stable sprite and can therefore share implementation logic. /// - public class SkinnableSprite : SkinnableDrawable + public class SkinnableSprite : SkinnableDrawable, ISkinnableDrawable { protected override bool ApplySizeRestrictionsToDefault => true; @@ -42,5 +42,7 @@ namespace osu.Game.Skinning public string LookupName { get; } } + + public bool UsesFixedAnchor { get; set; } } } From 66f5eae530cac55975b46655825d2b159e73976d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 19:32:34 +0900 Subject: [PATCH 217/285] Hook up a dropdown to show all available sprites for the current skin --- .../Configuration/SettingSourceAttribute.cs | 1 + osu.Game/Overlays/Settings/SettingsItem.cs | 6 ++++++ .../Skinning/Components/SkinnableSprite.cs | 19 ++++++++++++++++++- osu.Game/Skinning/Editor/SkinEditor.cs | 2 +- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 4111a67b24..8c84707b88 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -88,6 +88,7 @@ namespace osu.Game.Configuration throw new InvalidOperationException($"{nameof(SettingSourceAttribute)} had an unsupported custom control type ({controlType.ReadableName()})"); var control = (Drawable)Activator.CreateInstance(controlType); + controlType.GetProperty(nameof(SettingsItem.Source))?.SetValue(control, obj); controlType.GetProperty(nameof(SettingsItem.LabelText))?.SetValue(control, attr.Label); controlType.GetProperty(nameof(SettingsItem.TooltipText))?.SetValue(control, attr.Description); controlType.GetProperty(nameof(SettingsItem.Current))?.SetValue(control, value); diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index e709be1343..6ac5351270 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Containers; @@ -24,6 +25,11 @@ namespace osu.Game.Overlays.Settings protected Drawable Control { get; } + /// + /// The source component if this was created via . + /// + public Drawable Source { get; internal set; } + private IHasCurrentValue controlWithCurrent => Control as IHasCurrentValue; protected override Container Content => FlowContent; diff --git a/osu.Game/Skinning/Components/SkinnableSprite.cs b/osu.Game/Skinning/Components/SkinnableSprite.cs index 292bbe2321..aa23e428d1 100644 --- a/osu.Game/Skinning/Components/SkinnableSprite.cs +++ b/osu.Game/Skinning/Components/SkinnableSprite.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -8,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; +using osu.Game.Overlays.Settings; namespace osu.Game.Skinning.Components { @@ -19,12 +22,14 @@ namespace osu.Game.Skinning.Components { public bool UsesFixedAnchor { get; set; } - [SettingSource("Sprite name", "The filename of the sprite")] + [SettingSource("Sprite name", "The filename of the sprite", SettingControlType = typeof(SpriteSelectorControl))] public Bindable SpriteName { get; } = new Bindable(string.Empty); [Resolved] private ISkinSource source { get; set; } + public IEnumerable AvailableFiles => (source.AllSources.First() as Skin)?.SkinInfo.PerformRead(s => s.Files.Select(f => f.Filename)); + public SkinSprite() { AutoSizeAxes = Axes.Both; @@ -45,5 +50,17 @@ namespace osu.Game.Skinning.Components }; }, true); } + + public class SpriteSelectorControl : SettingsDropdown + { + public SkinSprite Source { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Items = Source.AvailableFiles; + } + } } } diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 7701fafbfc..392cb2f32b 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -351,7 +351,7 @@ namespace osu.Game.Skinning.Editor // place component placeComponent(new SkinSprite { - SpriteName = { Value = Path.GetFileNameWithoutExtension(file.Name) } + SpriteName = { Value = file.Name } }); }); From 9c3dad9fbf72f63cbb79fdc8f2ebc687a2d66f69 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 19:09:17 +0900 Subject: [PATCH 218/285] Add proof of concept flow to ensure `RealmBackedResourceStore` is invalidated on realm file changes I'm not at all happy with this, but it does work so let's go with it for now. --- osu.Game/Skinning/LegacyBeatmapSkin.cs | 5 +- osu.Game/Skinning/RealmBackedResourceStore.cs | 57 ++++++++++++------- osu.Game/Skinning/Skin.cs | 8 ++- osu.Game/Skinning/SkinManager.cs | 21 ++++++- 4 files changed, 67 insertions(+), 24 deletions(-) diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 16a05f4197..70f5b35d00 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -11,6 +11,7 @@ using osu.Framework.IO.Stores; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; +using osu.Game.Database; using osu.Game.IO; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; @@ -37,11 +38,11 @@ namespace osu.Game.Skinning private static IResourceStore createRealmBackedStore(BeatmapInfo beatmapInfo, IStorageResourceProvider? resources) { - if (resources == null) + if (resources == null || beatmapInfo.BeatmapSet == null) // should only ever be used in tests. return new ResourceStore(); - return new RealmBackedResourceStore(beatmapInfo.BeatmapSet, resources.Files, new[] { @"ogg" }); + return new RealmBackedResourceStore(beatmapInfo.BeatmapSet.ToLive(resources.RealmAccess), resources.Files, resources.RealmAccess); } public override Drawable? GetDrawableComponent(ISkinComponent component) diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index fc9036727f..115d563575 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -1,51 +1,68 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Game.Database; using osu.Game.Extensions; +using Realms; namespace osu.Game.Skinning { - public class RealmBackedResourceStore : ResourceStore + public class RealmBackedResourceStore : ResourceStore + where T : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey { - private readonly Dictionary fileToStoragePathMapping = new Dictionary(); + private Lazy> fileToStoragePathMapping; - public RealmBackedResourceStore(IHasRealmFiles source, IResourceStore underlyingStore, string[] extensions = null) + private readonly Live liveSource; + + public RealmBackedResourceStore(Live source, IResourceStore underlyingStore, RealmAccess? realm) : base(underlyingStore) { - // Must be initialised before the file cache. - if (extensions != null) - { - foreach (string extension in extensions) - AddExtension(extension); - } + liveSource = source; - initialiseFileCache(source); + invalidateCache(); + Debug.Assert(fileToStoragePathMapping != null); } - private void initialiseFileCache(IHasRealmFiles source) - { - fileToStoragePathMapping.Clear(); - foreach (var f in source.Files) - fileToStoragePathMapping[f.Filename.ToLowerInvariant()] = f.File.GetStoragePath(); - } + public void Invalidate() => invalidateCache(); protected override IEnumerable GetFilenames(string name) { foreach (string filename in base.GetFilenames(name)) { - string path = getPathForFile(filename.ToStandardisedPath()); + string? path = getPathForFile(filename.ToStandardisedPath()); if (path != null) yield return path; } } - private string getPathForFile(string filename) => - fileToStoragePathMapping.TryGetValue(filename.ToLower(), out string path) ? path : null; + private string? getPathForFile(string filename) + { + if (fileToStoragePathMapping.Value.TryGetValue(filename.ToLowerInvariant(), out string path)) + return path; - public override IEnumerable GetAvailableResources() => fileToStoragePathMapping.Keys; + return null; + } + + private void invalidateCache() => fileToStoragePathMapping = new Lazy>(initialiseFileCache, LazyThreadSafetyMode.ExecutionAndPublication); + + private Dictionary initialiseFileCache() => liveSource.PerformRead(source => + { + var dictionary = new Dictionary(); + dictionary.Clear(); + foreach (var f in source.Files) + dictionary[f.Filename.ToLowerInvariant()] = f.File.GetStoragePath(); + + return dictionary; + }); + + public override IEnumerable GetAvailableResources() => fileToStoragePathMapping.Value.Keys; } } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 2f01bb7301..fb9914cd9e 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -54,6 +54,10 @@ namespace osu.Game.Skinning where TLookup : notnull where TValue : notnull; + public void InvalidateCaches() => realmBackedStorage?.Invalidate(); + + private readonly RealmBackedResourceStore realmBackedStorage; + /// /// Construct a new skin. /// @@ -67,7 +71,9 @@ namespace osu.Game.Skinning { SkinInfo = skin.ToLive(resources.RealmAccess); - storage ??= new RealmBackedResourceStore(skin, resources.Files, new[] { @"ogg" }); + storage ??= realmBackedStorage = new RealmBackedResourceStore(SkinInfo, resources.Files, resources.RealmAccess); + + (storage as ResourceStore)?.AddExtension("ogg"); var samples = resources.AudioManager?.GetSampleStore(storage); if (samples != null) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 5333b58625..bafb088f68 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Threading; @@ -26,6 +27,7 @@ using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Models; using osu.Game.Overlays.Notifications; +using Realms; namespace osu.Game.Skinning { @@ -59,6 +61,8 @@ namespace osu.Game.Skinning private readonly IResourceStore userFiles; + private IDisposable currentSkinSubscription; + /// /// The default skin. /// @@ -97,7 +101,16 @@ namespace osu.Game.Skinning } }); - CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin); + CurrentSkinInfo.ValueChanged += skin => + { + CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin); + + scheduler.Add(() => + { + currentSkinSubscription?.Dispose(); + currentSkinSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.ID == skin.NewValue.ID), realmSkinChanged); + }); + }; CurrentSkin.Value = DefaultSkin; CurrentSkin.ValueChanged += skin => @@ -109,6 +122,12 @@ namespace osu.Game.Skinning }; } + private void realmSkinChanged(IRealmCollection sender, ChangeSet changes, Exception error) where T : RealmObjectBase + { + Logger.Log("Detected a skin change"); + CurrentSkin.Value.InvalidateCaches(); + } + public void SelectRandomSkin() { realm.Run(r => From 762de3cc9780e76e744ea4e3c5d532e16117e4fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Mar 2022 22:53:49 +0900 Subject: [PATCH 219/285] Replace invalidation logic with local realm notification subscription --- osu.Game/Skinning/RealmBackedResourceStore.cs | 12 +++++++++++- osu.Game/Skinning/Skin.cs | 4 ++-- osu.Game/Skinning/SkinManager.cs | 16 ---------------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index 115d563575..e727a7e59a 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading; using osu.Framework.Extensions; using osu.Framework.IO.Stores; @@ -21,6 +22,7 @@ namespace osu.Game.Skinning private Lazy> fileToStoragePathMapping; private readonly Live liveSource; + private readonly IDisposable? realmSubscription; public RealmBackedResourceStore(Live source, IResourceStore underlyingStore, RealmAccess? realm) : base(underlyingStore) @@ -29,9 +31,17 @@ namespace osu.Game.Skinning invalidateCache(); Debug.Assert(fileToStoragePathMapping != null); + + realmSubscription = realm?.RegisterForNotifications(r => r.All().Where(s => s.ID == source.ID), skinChanged); } - public void Invalidate() => invalidateCache(); + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + realmSubscription?.Dispose(); + } + + private void skinChanged(IRealmCollection sender, ChangeSet changes, Exception error) => invalidateCache(); protected override IEnumerable GetFilenames(string name) { diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index fb9914cd9e..4cd1d952db 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -54,8 +54,6 @@ namespace osu.Game.Skinning where TLookup : notnull where TValue : notnull; - public void InvalidateCaches() => realmBackedStorage?.Invalidate(); - private readonly RealmBackedResourceStore realmBackedStorage; /// @@ -206,6 +204,8 @@ namespace osu.Game.Skinning Textures?.Dispose(); Samples?.Dispose(); + + realmBackedStorage?.Dispose(); } #endregion diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index bafb088f68..5e85f9e4ca 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -16,7 +16,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; -using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Threading; @@ -27,7 +26,6 @@ using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Models; using osu.Game.Overlays.Notifications; -using Realms; namespace osu.Game.Skinning { @@ -61,8 +59,6 @@ namespace osu.Game.Skinning private readonly IResourceStore userFiles; - private IDisposable currentSkinSubscription; - /// /// The default skin. /// @@ -104,12 +100,6 @@ namespace osu.Game.Skinning CurrentSkinInfo.ValueChanged += skin => { CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin); - - scheduler.Add(() => - { - currentSkinSubscription?.Dispose(); - currentSkinSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.ID == skin.NewValue.ID), realmSkinChanged); - }); }; CurrentSkin.Value = DefaultSkin; @@ -122,12 +112,6 @@ namespace osu.Game.Skinning }; } - private void realmSkinChanged(IRealmCollection sender, ChangeSet changes, Exception error) where T : RealmObjectBase - { - Logger.Log("Detected a skin change"); - CurrentSkin.Value.InvalidateCaches(); - } - public void SelectRandomSkin() { realm.Run(r => From d1be229d74806794f8f136f3919885f915c118fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 17:31:03 +0900 Subject: [PATCH 220/285] Combine `SkinSprite` into `SkinnableSprite` --- .../Skinning/Components/SkinnableSprite.cs | 66 ------------------- osu.Game/Skinning/DefaultSkin.cs | 4 ++ osu.Game/Skinning/Editor/SkinEditor.cs | 5 +- osu.Game/Skinning/Skin.cs | 2 +- osu.Game/Skinning/SkinnableDrawable.cs | 8 +-- osu.Game/Skinning/SkinnableSprite.cs | 47 ++++++++++++- 6 files changed, 55 insertions(+), 77 deletions(-) delete mode 100644 osu.Game/Skinning/Components/SkinnableSprite.cs diff --git a/osu.Game/Skinning/Components/SkinnableSprite.cs b/osu.Game/Skinning/Components/SkinnableSprite.cs deleted file mode 100644 index aa23e428d1..0000000000 --- a/osu.Game/Skinning/Components/SkinnableSprite.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Game.Configuration; -using osu.Game.Overlays.Settings; - -namespace osu.Game.Skinning.Components -{ - /// - /// Intended to be a test bed for skinning. May be removed at some point in the future. - /// - [UsedImplicitly] - public class SkinSprite : CompositeDrawable, ISkinnableDrawable - { - public bool UsesFixedAnchor { get; set; } - - [SettingSource("Sprite name", "The filename of the sprite", SettingControlType = typeof(SpriteSelectorControl))] - public Bindable SpriteName { get; } = new Bindable(string.Empty); - - [Resolved] - private ISkinSource source { get; set; } - - public IEnumerable AvailableFiles => (source.AllSources.First() as Skin)?.SkinInfo.PerformRead(s => s.Files.Select(f => f.Filename)); - - public SkinSprite() - { - AutoSizeAxes = Axes.Both; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - SpriteName.BindValueChanged(spriteName => - { - InternalChildren = new Drawable[] - { - new Sprite - { - Texture = source.GetTexture(SpriteName.Value), - } - }; - }, true); - } - - public class SpriteSelectorControl : SettingsDropdown - { - public SkinSprite Source { get; set; } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Items = Source.AvailableFiles; - } - } - } -} diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 43ada59bcb..c645b0fae4 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -8,6 +8,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; @@ -157,6 +158,9 @@ namespace osu.Game.Skinning break; } + if (GetTexture(component.LookupName) is Texture t) + return new Sprite { Texture = t }; + return null; } diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 392cb2f32b..484faebdc0 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -21,7 +21,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; -using osu.Game.Skinning.Components; namespace osu.Game.Skinning.Editor { @@ -349,9 +348,9 @@ namespace osu.Game.Skinning.Editor }); // place component - placeComponent(new SkinSprite + placeComponent(new SkinnableSprite { - SpriteName = { Value = file.Name } + SpriteName = { Value = Path.GetFileNameWithoutExtension(file.Name) } }); }); diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 4cd1d952db..f2d095f880 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -54,7 +54,7 @@ namespace osu.Game.Skinning where TLookup : notnull where TValue : notnull; - private readonly RealmBackedResourceStore realmBackedStorage; + private readonly RealmBackedResourceStore? realmBackedStorage; /// /// Construct a new skin. diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs index 72f64e2e12..45409694b5 100644 --- a/osu.Game/Skinning/SkinnableDrawable.cs +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -31,7 +31,7 @@ namespace osu.Game.Skinning set => base.AutoSizeAxes = value; } - private readonly ISkinComponent component; + protected readonly ISkinComponent Component; private readonly ConfineMode confineMode; @@ -49,7 +49,7 @@ namespace osu.Game.Skinning protected SkinnableDrawable(ISkinComponent component, ConfineMode confineMode = ConfineMode.NoScaling) { - this.component = component; + Component = component; this.confineMode = confineMode; RelativeSizeAxes = Axes.Both; @@ -75,13 +75,13 @@ namespace osu.Game.Skinning protected override void SkinChanged(ISkinSource skin) { - Drawable = skin.GetDrawableComponent(component); + Drawable = skin.GetDrawableComponent(Component); isDefault = false; if (Drawable == null) { - Drawable = CreateDefault(component); + Drawable = CreateDefault(Component); isDefault = true; } diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index 38803bd8e3..aa3001fe45 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -1,10 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.IO; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Game.Configuration; +using osu.Game.Overlays.Settings; namespace osu.Game.Skinning { @@ -18,9 +24,32 @@ namespace osu.Game.Skinning [Resolved] private TextureStore textures { get; set; } + [SettingSource("Sprite name", "The filename of the sprite", SettingControlType = typeof(SpriteSelectorControl))] + public Bindable SpriteName { get; } = new Bindable(string.Empty); + + [Resolved] + private ISkinSource source { get; set; } + + public IEnumerable AvailableFiles => (source.AllSources.First() as Skin)?.SkinInfo.PerformRead(s => s.Files.Select(f => Path.GetFileNameWithoutExtension(f.Filename)).Distinct()); + public SkinnableSprite(string textureName, ConfineMode confineMode = ConfineMode.NoScaling) : base(new SpriteComponent(textureName), confineMode) { + SpriteName.Value = textureName; + } + + public SkinnableSprite() + : base(new SpriteComponent(string.Empty), ConfineMode.NoScaling) + { + RelativeSizeAxes = Axes.None; + AutoSizeAxes = Axes.Both; + + SpriteName.BindValueChanged(name => + { + ((SpriteComponent)Component).LookupName = name.NewValue ?? string.Empty; + if (IsLoaded) + SkinChanged(CurrentSkin); + }); } protected override Drawable CreateDefault(ISkinComponent component) @@ -33,16 +62,28 @@ namespace osu.Game.Skinning return new Sprite { Texture = texture }; } + public bool UsesFixedAnchor { get; set; } + private class SpriteComponent : ISkinComponent { + public string LookupName { get; set; } + public SpriteComponent(string textureName) { LookupName = textureName; } - - public string LookupName { get; } } - public bool UsesFixedAnchor { get; set; } + public class SpriteSelectorControl : SettingsDropdown + { + public SkinnableSprite Source { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Items = Source.AvailableFiles; + } + } } } From 52eeaffce333df069e325c9b4bc69b0ed00bc4d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Mar 2022 17:36:02 +0900 Subject: [PATCH 221/285] Limit lookup resources to images --- osu.Game/Skinning/SkinnableSprite.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index aa3001fe45..f36ae89e25 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.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; using System.Collections.Generic; -using System.IO; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -30,7 +30,12 @@ namespace osu.Game.Skinning [Resolved] private ISkinSource source { get; set; } - public IEnumerable AvailableFiles => (source.AllSources.First() as Skin)?.SkinInfo.PerformRead(s => s.Files.Select(f => Path.GetFileNameWithoutExtension(f.Filename)).Distinct()); + public IEnumerable AvailableFiles => (source.AllSources.First() as Skin)?.SkinInfo.PerformRead(s => s.Files + .Where(f => + f.Filename.EndsWith(".png", StringComparison.Ordinal) + || f.Filename.EndsWith(".jpg", StringComparison.Ordinal) + ) + .Select(f => f.Filename).Distinct()); public SkinnableSprite(string textureName, ConfineMode confineMode = ConfineMode.NoScaling) : base(new SpriteComponent(textureName), confineMode) From 2b7105ac4fc0ab4663a5e7d3fd72c836028a4714 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Apr 2022 14:46:14 +0900 Subject: [PATCH 222/285] Add a default sprite representation to allow better placeholder display in skin editor toolbox --- osu.Game/Skinning/SkinnableSprite.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index f36ae89e25..0005045c00 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Configuration; using osu.Game.Overlays.Settings; +using osuTK; namespace osu.Game.Skinning { @@ -62,7 +63,13 @@ namespace osu.Game.Skinning var texture = textures.Get(component.LookupName); if (texture == null) - return null; + { + return new SpriteIcon + { + Size = new Vector2(100), + Icon = FontAwesome.Solid.QuestionCircle + }; + } return new Sprite { Texture = texture }; } @@ -87,7 +94,8 @@ namespace osu.Game.Skinning { base.LoadComplete(); - Items = Source.AvailableFiles; + if (Source.AvailableFiles.Any()) + Items = Source.AvailableFiles; } } } From 314ad63c6eee361142d0129bab387301bf442231 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Apr 2022 15:14:53 +0900 Subject: [PATCH 223/285] Simplify available file lookup and include file extension --- osu.Game/Skinning/Editor/SkinEditor.cs | 2 +- osu.Game/Skinning/SkinnableSprite.cs | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 484faebdc0..d36806a1b3 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -350,7 +350,7 @@ namespace osu.Game.Skinning.Editor // place component placeComponent(new SkinnableSprite { - SpriteName = { Value = Path.GetFileNameWithoutExtension(file.Name) } + SpriteName = { Value = file.Name } }); }); diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index 0005045c00..87490b4397 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -31,13 +30,6 @@ namespace osu.Game.Skinning [Resolved] private ISkinSource source { get; set; } - public IEnumerable AvailableFiles => (source.AllSources.First() as Skin)?.SkinInfo.PerformRead(s => s.Files - .Where(f => - f.Filename.EndsWith(".png", StringComparison.Ordinal) - || f.Filename.EndsWith(".jpg", StringComparison.Ordinal) - ) - .Select(f => f.Filename).Distinct()); - public SkinnableSprite(string textureName, ConfineMode confineMode = ConfineMode.NoScaling) : base(new SpriteComponent(textureName), confineMode) { @@ -88,14 +80,22 @@ namespace osu.Game.Skinning public class SpriteSelectorControl : SettingsDropdown { - public SkinnableSprite Source { get; set; } - protected override void LoadComplete() { base.LoadComplete(); - if (Source.AvailableFiles.Any()) - Items = Source.AvailableFiles; + // Round-about way of getting the user's skin to find available resources. + // In the future we'll probably want to allow access to resources from the fallbacks, or potentially other skins + // but that requires further thought. + var highestPrioritySkin = ((SkinnableSprite)Source).source.AllSources.First() as Skin; + + string[] availableFiles = highestPrioritySkin?.SkinInfo.PerformRead(s => s.Files + .Where(f => f.Filename.EndsWith(".png", StringComparison.Ordinal) + || f.Filename.EndsWith(".jpg", StringComparison.Ordinal)) + .Select(f => f.Filename).Distinct()).ToArray(); + + if (availableFiles?.Length > 0) + Items = availableFiles; } } } From bfd3406f5f69149a251112ce7f3d94ee81fecce0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Apr 2022 15:49:05 +0900 Subject: [PATCH 224/285] Ensure that file is imported and caches are invalidated before placing new sprites --- osu.Game/Skinning/Editor/SkinEditor.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index d36806a1b3..df0bb7a70c 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -48,6 +48,9 @@ namespace osu.Game.Skinning.Editor [Resolved] private OsuColour colours { get; set; } + [Resolved] + private RealmAccess realm { get; set; } + [Resolved(canBeNull: true)] private SkinEditorOverlay skinEditorOverlay { get; set; } @@ -347,6 +350,11 @@ namespace osu.Game.Skinning.Editor skins.AddFile(skinInfo, contents, file.Name); }); + // Even though we are 100% on an update thread, we need to wait for realm callbacks to fire (to correctly invalidate caches in RealmBackedResourceStore). + // See https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-2483573 for further discussion. + // This is the best we can do for now. + realm.Run(r => r.Refresh()); + // place component placeComponent(new SkinnableSprite { From 6afed5e865ff278970db643719f1377c8cb08660 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Apr 2022 16:00:51 +0900 Subject: [PATCH 225/285] Fix new `SettingsItem` attribute not playing well with non-`Drawable`s --- osu.Game/Configuration/SettingSourceAttribute.cs | 2 +- osu.Game/Overlays/Settings/SettingsItem.cs | 2 +- osu.Game/Skinning/SkinnableSprite.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 8c84707b88..89f0e73f4f 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -88,7 +88,7 @@ namespace osu.Game.Configuration throw new InvalidOperationException($"{nameof(SettingSourceAttribute)} had an unsupported custom control type ({controlType.ReadableName()})"); var control = (Drawable)Activator.CreateInstance(controlType); - controlType.GetProperty(nameof(SettingsItem.Source))?.SetValue(control, obj); + controlType.GetProperty(nameof(SettingsItem.SettingSourceObject))?.SetValue(control, obj); controlType.GetProperty(nameof(SettingsItem.LabelText))?.SetValue(control, attr.Label); controlType.GetProperty(nameof(SettingsItem.TooltipText))?.SetValue(control, attr.Description); controlType.GetProperty(nameof(SettingsItem.Current))?.SetValue(control, value); diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 6ac5351270..098090bf78 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Settings /// /// The source component if this was created via . /// - public Drawable Source { get; internal set; } + public object SettingSourceObject { get; internal set; } private IHasCurrentValue controlWithCurrent => Control as IHasCurrentValue; diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index 87490b4397..c6cc4c1bdd 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -87,7 +87,7 @@ namespace osu.Game.Skinning // Round-about way of getting the user's skin to find available resources. // In the future we'll probably want to allow access to resources from the fallbacks, or potentially other skins // but that requires further thought. - var highestPrioritySkin = ((SkinnableSprite)Source).source.AllSources.First() as Skin; + var highestPrioritySkin = ((SkinnableSprite)SettingSourceObject).source.AllSources.First() as Skin; string[] availableFiles = highestPrioritySkin?.SkinInfo.PerformRead(s => s.Files .Where(f => f.Filename.EndsWith(".png", StringComparison.Ordinal) From f0821ce1fc9ca72c3e1c1534e50ebd913e768a77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Apr 2022 16:16:49 +0900 Subject: [PATCH 226/285] Import new skin editor sprites to the cursor location --- osu.Game/Skinning/Editor/SkinEditor.cs | 25 +++++++++++++------ .../Skinning/Editor/SkinSelectionHandler.cs | 8 +++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index df0bb7a70c..0208f109da 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -254,7 +254,7 @@ namespace osu.Game.Skinning.Editor placeComponent(component); } - private void placeComponent(ISkinnableDrawable component) + private void placeComponent(ISkinnableDrawable component, bool applyDefaults = true) { var targetContainer = getFirstTarget(); @@ -263,10 +263,13 @@ namespace osu.Game.Skinning.Editor var drawableComponent = (Drawable)component; - // give newly added components a sane starting location. - drawableComponent.Origin = Anchor.TopCentre; - drawableComponent.Anchor = Anchor.TopCentre; - drawableComponent.Y = targetContainer.DrawSize.Y / 2; + if (applyDefaults) + { + // give newly added components a sane starting location. + drawableComponent.Origin = Anchor.TopCentre; + drawableComponent.Anchor = Anchor.TopCentre; + drawableComponent.Y = targetContainer.DrawSize.Y / 2; + } targetContainer.Add(component); @@ -356,10 +359,16 @@ namespace osu.Game.Skinning.Editor realm.Run(r => r.Refresh()); // place component - placeComponent(new SkinnableSprite + var sprite = new SkinnableSprite { - SpriteName = { Value = file.Name } - }); + SpriteName = { Value = file.Name }, + Origin = Anchor.Centre, + Position = getFirstTarget().ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position), + }; + + placeComponent(sprite, false); + + SkinSelectionHandler.ApplyClosestAnchor(sprite); }); return Task.CompletedTask; diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index d7fb5c0498..943425e099 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -157,13 +157,13 @@ namespace osu.Game.Skinning.Editor if (item.UsesFixedAnchor) continue; - applyClosestAnchor(drawable); + ApplyClosestAnchor(drawable); } return true; } - private static void applyClosestAnchor(Drawable drawable) => applyAnchor(drawable, getClosestAnchor(drawable)); + public static void ApplyClosestAnchor(Drawable drawable) => applyAnchor(drawable, getClosestAnchor(drawable)); protected override void OnSelectionChanged() { @@ -252,7 +252,7 @@ namespace osu.Game.Skinning.Editor if (item.UsesFixedAnchor) continue; - applyClosestAnchor(drawable); + ApplyClosestAnchor(drawable); } } @@ -279,7 +279,7 @@ namespace osu.Game.Skinning.Editor foreach (var item in SelectedItems) { item.UsesFixedAnchor = false; - applyClosestAnchor((Drawable)item); + ApplyClosestAnchor((Drawable)item); } } From 01681ee8749c3562672e20fe603597c39a467410 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Apr 2022 16:19:00 +0900 Subject: [PATCH 227/285] Add missing `ToArray` call Not sure where this went, was there in my original commit. --- osu.Game/Skinning/SkinManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 44b9c69794..832cb01d22 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -145,10 +145,10 @@ namespace osu.Game.Skinning if (!s.Protected) return; - var existingSkinNames = realm.Run(r => r.All() - .Where(skin => !skin.DeletePending) - .AsEnumerable() - .Select(skin => skin.Name)); + string[] existingSkinNames = realm.Run(r => r.All() + .Where(skin => !skin.DeletePending) + .AsEnumerable() + .Select(skin => skin.Name)).ToArray(); // if the user is attempting to save one of the default skin implementations, create a copy first. var skinInfo = new SkinInfo From fc3ebe9b510d34b5d75aae931076328b09851d9d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 1 Apr 2022 10:40:16 +0300 Subject: [PATCH 228/285] Reword log retrieval steps on mobile platforms Co-authored-by: Dean Herbert --- .github/ISSUE_TEMPLATE/bug-issue.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml index ea5ee298fb..91ca622f55 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.yml +++ b/.github/ISSUE_TEMPLATE/bug-issue.yml @@ -65,8 +65,8 @@ body: ### Mobile platforms The places to find the logs on mobile platforms are as follows: - - `Android/data/sh.ppy.osulazer/files/logs` *on Android* - - *On iOS*, they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer) + - *On Android*, navigate to `Android/data/sh.ppy.osulazer/files/logs` using a file browser app. + - *On iOS*, connect your device to a PC and copy the `logs` directory from the app's document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer) --- From 37dea0ff211db68d591a98a008cc3e4c76879ebb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 1 Apr 2022 17:05:11 +0900 Subject: [PATCH 229/285] Add failing test case --- .../TestSceneMultiplayerPlaylist.cs | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index cbd8b472b8..f8c0939ea9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -13,6 +14,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -183,14 +185,41 @@ namespace osu.Game.Tests.Visual.Multiplayer assertItemInHistoryListStep(2, 0); } + [Test] + public void TestInsertedItemDoesNotRefreshAllOthers() + { + AddStep("change to round robin queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayersRoundRobin }).WaitSafely()); + + // Add a few items for the local user. + addItemStep(); + addItemStep(); + addItemStep(); + addItemStep(); + addItemStep(); + + DrawableRoomPlaylistItem[] drawableItems = null; + AddStep("get drawable items", () => drawableItems = this.ChildrenOfType().ToArray()); + + // Add 1 item for another user. + AddStep("join second user", () => MultiplayerClient.AddUser(new APIUser { Id = 10 })); + addItemStep(userId: 10); + + // New item inserted towards the top of the list. + assertItemInQueueListStep(7, 1); + AddAssert("all previous playlist items remained", () => drawableItems.All(this.ChildrenOfType().Contains)); + } + /// /// Adds a step to create a new playlist item. /// - private void addItemStep(bool expired = false) => AddStep("add item", () => MultiplayerClient.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap) + private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () => { - Expired = expired, - PlayedAt = DateTimeOffset.Now - }))); + MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap) + { + Expired = expired, + PlayedAt = DateTimeOffset.Now + })).WaitSafely(); + }); /// /// Asserts the position of a given playlist item in the queue list. From 16d4544ff9b4ca72fcd0899c9cd56a1a92f91246 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 1 Apr 2022 17:06:37 +0900 Subject: [PATCH 230/285] Prevent reloads when playlist item order changes --- osu.Game/Online/Rooms/PlaylistItem.cs | 6 ++++-- .../Match/Playlist/MultiplayerPlaylist.cs | 20 +++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index f696362cbb..a56851cfe6 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Utils; namespace osu.Game.Online.Rooms { @@ -101,13 +102,13 @@ namespace osu.Game.Online.Rooms #endregion - public PlaylistItem With(IBeatmapInfo beatmap) => new PlaylistItem(beatmap) + public PlaylistItem With(Optional beatmap = default, Optional playlistOrder = default) => new PlaylistItem(beatmap.GetOr(Beatmap)) { ID = ID, OwnerID = OwnerID, RulesetID = RulesetID, Expired = Expired, - PlaylistOrder = PlaylistOrder, + PlaylistOrder = playlistOrder.GetOr(PlaylistOrder), PlayedAt = PlayedAt, AllowedMods = AllowedMods, RequiredMods = RequiredMods, @@ -119,6 +120,7 @@ namespace osu.Game.Online.Rooms && Beatmap.OnlineID == other.Beatmap.OnlineID && RulesetID == other.RulesetID && Expired == other.Expired + && PlaylistOrder == other.PlaylistOrder && AllowedMods.SequenceEqual(other.AllowedMods) && RequiredMods.SequenceEqual(other.RequiredMods); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index 879a21e7c1..41f548a630 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -117,8 +117,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { base.PlaylistItemChanged(item); - removeItemFromLists(item.ID); - addItemToLists(item); + var newApiItem = Playlist.SingleOrDefault(i => i.ID == item.ID); + var existingApiItemInQueue = queueList.Items.SingleOrDefault(i => i.ID == item.ID); + + // Test if the only change between the two playlist items is the order. + if (newApiItem != null && existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem)) + { + // Set the new playlist order directly without refreshing the DrawablePlaylistItem. + existingApiItemInQueue.PlaylistOrder = newApiItem.PlaylistOrder; + + // The following isn't really required, but is here for safety and explicitness. + // MultiplayerQueueList internally binds to changes in Playlist to invalidate its own layout, which is mutated on every playlist operation. + queueList.Invalidate(); + } + else + { + removeItemFromLists(item.ID); + addItemToLists(item); + } } private void addItemToLists(MultiplayerPlaylistItem item) From 6e6271d0c0ac4e00b0b2fd18b124e3cd4a0a6ca2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 1 Apr 2022 18:31:17 +0900 Subject: [PATCH 231/285] Fix "server-side" room playlist not updated Remove unused using --- .../TestSceneMultiplayerPlaylist.cs | 1 - osu.Game/Online/Rooms/PlaylistItem.cs | 13 ++++++++ .../Multiplayer/TestMultiplayerClient.cs | 33 +++++++++++++++---- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index f8c0939ea9..1231866b36 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index a56851cfe6..6ec884d79c 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -85,6 +85,19 @@ namespace osu.Game.Online.Rooms Beatmap = beatmap; } + public PlaylistItem(MultiplayerPlaylistItem item) + : this(new APIBeatmap { OnlineID = item.BeatmapID }) + { + ID = item.ID; + OwnerID = item.OwnerID; + RulesetID = item.RulesetID; + Expired = item.Expired; + PlaylistOrder = item.PlaylistOrder; + PlayedAt = item.PlayedAt; + RequiredMods = item.RequiredMods.ToArray(); + AllowedMods = item.AllowedMods.ToArray(); + } + public void MarkInvalid() => valid.Value = false; #region Newtonsoft.Json implicit ShouldSerialize() methods diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index b9304f713d..0efaf16f99 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -31,7 +31,11 @@ namespace osu.Game.Tests.Visual.Multiplayer public override IBindable IsConnected => isConnected; private readonly Bindable isConnected = new Bindable(true); + /// + /// The local client's . This is not always equivalent to the server-side room. + /// public new Room? APIRoom => base.APIRoom; + public Action? RoomSetupAction; public bool RoomJoined { get; private set; } @@ -46,6 +50,11 @@ namespace osu.Game.Tests.Visual.Multiplayer /// private readonly List serverSidePlaylist = new List(); + /// + /// Guaranteed up-to-date API room. + /// + private Room? serverSideAPIRoom; + private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex]; private int currentIndex; private long lastPlaylistItemId; @@ -192,13 +201,13 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override async Task JoinRoom(long roomId, string? password = null) { - var apiRoom = roomManager.ServerSideRooms.Single(r => r.RoomID.Value == roomId); + serverSideAPIRoom = roomManager.ServerSideRooms.Single(r => r.RoomID.Value == roomId); - if (password != apiRoom.Password.Value) + if (password != serverSideAPIRoom.Password.Value) throw new InvalidOperationException("Invalid password."); serverSidePlaylist.Clear(); - serverSidePlaylist.AddRange(apiRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item))); + serverSidePlaylist.AddRange(serverSideAPIRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item))); lastPlaylistItemId = serverSidePlaylist.Max(item => item.ID); var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id) @@ -210,11 +219,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { Settings = { - Name = apiRoom.Name.Value, - MatchType = apiRoom.Type.Value, + Name = serverSideAPIRoom.Name.Value, + MatchType = serverSideAPIRoom.Type.Value, Password = password, - QueueMode = apiRoom.QueueMode.Value, - AutoStartDuration = apiRoom.AutoStartDuration.Value + QueueMode = serverSideAPIRoom.QueueMode.Value, + AutoStartDuration = serverSideAPIRoom.AutoStartDuration.Value }, Playlist = serverSidePlaylist.ToList(), Users = { localUser }, @@ -479,6 +488,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Debug.Assert(Room != null); Debug.Assert(APIRoom != null); + Debug.Assert(serverSideAPIRoom != null); var item = serverSidePlaylist.Find(i => i.ID == playlistItemId); @@ -495,6 +505,7 @@ namespace osu.Game.Tests.Visual.Multiplayer throw new InvalidOperationException("Attempted to remove an item which has already been played."); serverSidePlaylist.Remove(item); + serverSideAPIRoom.Playlist.RemoveAll(i => i.ID == item.ID); await ((IMultiplayerClient)this).PlaylistItemRemoved(playlistItemId).ConfigureAwait(false); await updateCurrentItem(Room).ConfigureAwait(false); @@ -576,10 +587,12 @@ namespace osu.Game.Tests.Visual.Multiplayer private async Task addItem(MultiplayerPlaylistItem item) { Debug.Assert(Room != null); + Debug.Assert(serverSideAPIRoom != null); item.ID = ++lastPlaylistItemId; serverSidePlaylist.Add(item); + serverSideAPIRoom.Playlist.Add(new PlaylistItem(item)); await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false); await updatePlaylistOrder(Room).ConfigureAwait(false); @@ -603,6 +616,8 @@ namespace osu.Game.Tests.Visual.Multiplayer private async Task updatePlaylistOrder(MultiplayerRoom room) { + Debug.Assert(serverSideAPIRoom != null); + List orderedActiveItems; switch (room.Settings.QueueMode) @@ -648,6 +663,10 @@ namespace osu.Game.Tests.Visual.Multiplayer await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); } + + // Also ensure that the API room's playlist is correct. + foreach (var item in serverSideAPIRoom.Playlist) + item.PlaylistOrder = serverSidePlaylist.Single(i => i.ID == item.ID).PlaylistOrder; } } } From 43d03f28255858659aac9c2f5078593ba0ecfd24 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Apr 2022 19:30:16 +0900 Subject: [PATCH 232/285] Put `ToArray` call in correct place in brackets --- osu.Game/Skinning/SkinManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 832cb01d22..71920fb166 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -148,7 +148,7 @@ namespace osu.Game.Skinning string[] existingSkinNames = realm.Run(r => r.All() .Where(skin => !skin.DeletePending) .AsEnumerable() - .Select(skin => skin.Name)).ToArray(); + .Select(skin => skin.Name).ToArray()); // if the user is attempting to save one of the default skin implementations, create a copy first. var skinInfo = new SkinInfo From 0f4b75ab15241c71ac89389a19586cd04ee821b7 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 1 Apr 2022 21:33:57 +0900 Subject: [PATCH 233/285] Add multiplayer lobby countdown SFX --- .../Match/MultiplayerReadyButton.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 62be9ad3bd..8068e80534 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -5,6 +5,8 @@ using System; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Localisation; using osu.Framework.Threading; using osu.Game.Graphics; @@ -27,6 +29,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [CanBeNull] private MultiplayerRoom room => multiplayerClient.Room; + private Sample countdownTickSample; + private Sample countdownTickFinalSample; + private int? lastTickPlayed; + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + countdownTickSample = audio.Samples.Get(@"Multiplayer/countdown-tick"); + countdownTickFinalSample = audio.Samples.Get(@"Multiplayer/countdown-tick-final"); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -83,6 +96,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match else countdownRemaining = countdown.TimeRemaining - timeElapsed; + if (countdownRemaining.Seconds <= 10 && (lastTickPlayed == null || lastTickPlayed != countdownRemaining.Seconds)) + { + countdownTickSample?.Play(); + + if (countdownRemaining.Seconds <= 3) + countdownTickFinalSample?.Play(); + + lastTickPlayed = countdownRemaining.Seconds; + } + string countdownText = $"Starting in {countdownRemaining:mm\\:ss}"; switch (localUser?.State) From b07152a119d3df101600ff18356c476ffdce9326 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Sat, 2 Apr 2022 02:35:37 +0200 Subject: [PATCH 234/285] Initial attempt at writing a test for the toolbarClock I'll add more once I know if this code passes review or not --- .../Visual/Menus/TestSceneToolbarClock.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs index 064d6f82fd..0b5904aa2b 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs @@ -16,6 +16,7 @@ namespace osu.Game.Tests.Visual.Menus public class TestSceneToolbarClock : OsuManualInputManagerTestScene { private readonly Container mainContainer; + private readonly ToolbarClock toolbarClock; public TestSceneToolbarClock() { @@ -49,7 +50,7 @@ namespace osu.Game.Tests.Visual.Menus RelativeSizeAxes = Axes.Y, Width = 2, }, - new ToolbarClock(), + toolbarClock = new ToolbarClock(), new Box { Colour = Color4.DarkRed, @@ -76,5 +77,22 @@ namespace osu.Game.Tests.Visual.Menus { AddStep("Set game time long", () => mainContainer.Clock = new FramedOffsetClock(Clock, false) { Offset = 3600.0 * 24 * 1000 * 98 }); } + + [Test] + public void TestHoverBackground() + { + Box hoverBackground = null; + + AddStep("Retrieve hover background", () => hoverBackground = (Box)toolbarClock.Children[0]); + + AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(mainContainer, new Vector2(0,200))); + AddAssert("Hover background is not visible", () => hoverBackground.Alpha == 0); + + AddStep("Move mouse on top of clock", () => InputManager.MoveMouseTo(mainContainer)); + AddAssert("Hover background is visible", () => hoverBackground.Alpha != 0); + + AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(mainContainer, new Vector2(0,200))); + AddUntilStep("Hover background is not visible", () => hoverBackground.Alpha == 0); + } } } From 6685c97147a92e71fa27b8632a69ae592ad535f4 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Sat, 2 Apr 2022 02:43:44 +0200 Subject: [PATCH 235/285] mainContainer -> toolbarClock --- osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs index 0b5904aa2b..a17e4030ff 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs @@ -85,13 +85,13 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Retrieve hover background", () => hoverBackground = (Box)toolbarClock.Children[0]); - AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(mainContainer, new Vector2(0,200))); + AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(toolbarClock, new Vector2(0,200))); AddAssert("Hover background is not visible", () => hoverBackground.Alpha == 0); - AddStep("Move mouse on top of clock", () => InputManager.MoveMouseTo(mainContainer)); + AddStep("Move mouse on top of clock", () => InputManager.MoveMouseTo(toolbarClock)); AddAssert("Hover background is visible", () => hoverBackground.Alpha != 0); - - AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(mainContainer, new Vector2(0,200))); + + AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(toolbarClock, new Vector2(0,200))); AddUntilStep("Hover background is not visible", () => hoverBackground.Alpha == 0); } } From dc744f18ff7700d1cadd4ac9ad7cfa883a3ecabc Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Sat, 2 Apr 2022 02:43:51 +0200 Subject: [PATCH 236/285] Trim whitespace --- osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs index a17e4030ff..b20ec7cbe5 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs @@ -87,10 +87,8 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(toolbarClock, new Vector2(0,200))); AddAssert("Hover background is not visible", () => hoverBackground.Alpha == 0); - AddStep("Move mouse on top of clock", () => InputManager.MoveMouseTo(toolbarClock)); AddAssert("Hover background is visible", () => hoverBackground.Alpha != 0); - AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(toolbarClock, new Vector2(0,200))); AddUntilStep("Hover background is not visible", () => hoverBackground.Alpha == 0); } From 9350f6f5f841684c70864ea64b059e29b0001f95 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Sat, 2 Apr 2022 02:59:07 +0200 Subject: [PATCH 237/285] Add spaces around commas in Vector2 construction --- osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs index b20ec7cbe5..df61e56011 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs @@ -85,11 +85,11 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Retrieve hover background", () => hoverBackground = (Box)toolbarClock.Children[0]); - AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(toolbarClock, new Vector2(0,200))); + AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(toolbarClock, new Vector2(0, 200))); AddAssert("Hover background is not visible", () => hoverBackground.Alpha == 0); AddStep("Move mouse on top of clock", () => InputManager.MoveMouseTo(toolbarClock)); AddAssert("Hover background is visible", () => hoverBackground.Alpha != 0); - AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(toolbarClock, new Vector2(0,200))); + AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(toolbarClock, new Vector2(0, 200))); AddUntilStep("Hover background is not visible", () => hoverBackground.Alpha == 0); } } From 95ccac50d4c1f2dfbeefbea492f69f3b06979830 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Sat, 2 Apr 2022 04:26:16 +0200 Subject: [PATCH 238/285] Add display mode changing test Yup this is gonna fail horribly --- .../Visual/Menus/TestSceneToolbarClock.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs index df61e56011..7643e5032b 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs @@ -2,10 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Timing; +using osu.Game.Configuration; using osu.Game.Overlays.Toolbar; using osuTK; using osuTK.Graphics; @@ -15,6 +18,8 @@ namespace osu.Game.Tests.Visual.Menus [TestFixture] public class TestSceneToolbarClock : OsuManualInputManagerTestScene { + private Bindable clockDisplayMode; + private readonly Container mainContainer; private readonly ToolbarClock toolbarClock; @@ -66,6 +71,12 @@ namespace osu.Game.Tests.Visual.Menus AddSliderStep("scale", 0.5, 4, 1, scale => mainContainer.Scale = new Vector2((float)scale)); } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + clockDisplayMode = config.GetBindable(OsuSetting.ToolbarClockDisplayMode); + } + [Test] public void TestRealGameTime() { @@ -92,5 +103,22 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(toolbarClock, new Vector2(0, 200))); AddUntilStep("Hover background is not visible", () => hoverBackground.Alpha == 0); } + + [Test] + public void TestDisplayModeChange() + { + ToolbarClockDisplayMode initialDisplayMode = 0; + + AddStep("Retrieve current state", () => initialDisplayMode = (ToolbarClockDisplayMode)clockDisplayMode.Value); + + AddStep("Trigger click", () => toolbarClock.TriggerClick()); + AddAssert("State changed from initial", () => clockDisplayMode.Value != initialDisplayMode); + AddStep("Trigger click", () => toolbarClock.TriggerClick()); + AddAssert("State changed from initial", () => clockDisplayMode.Value != initialDisplayMode); + AddStep("Trigger click", () => toolbarClock.TriggerClick()); + AddAssert("State changed from initial", () => clockDisplayMode.Value != initialDisplayMode); + AddStep("Trigger click", () => toolbarClock.TriggerClick()); + AddAssert("State is equal to initial", () => clockDisplayMode.Value == initialDisplayMode); + } } } From 245e452d41fcc2af69007f36f1f33122f2805bd5 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Sat, 2 Apr 2022 04:31:43 +0200 Subject: [PATCH 239/285] Remove redundant typecast I accidentally left in (Thanks InspectCode) --- osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs index 7643e5032b..c1b8cd919d 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs @@ -109,7 +109,7 @@ namespace osu.Game.Tests.Visual.Menus { ToolbarClockDisplayMode initialDisplayMode = 0; - AddStep("Retrieve current state", () => initialDisplayMode = (ToolbarClockDisplayMode)clockDisplayMode.Value); + AddStep("Retrieve current state", () => initialDisplayMode = clockDisplayMode.Value); AddStep("Trigger click", () => toolbarClock.TriggerClick()); AddAssert("State changed from initial", () => clockDisplayMode.Value != initialDisplayMode); From ee9696855ba9e1098d33353179f903283a2cb69a Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Sat, 2 Apr 2022 04:41:05 +0200 Subject: [PATCH 240/285] Remove hover test --- .../Visual/Menus/TestSceneToolbarClock.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs index c1b8cd919d..269b3cbad0 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs @@ -89,21 +89,6 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Set game time long", () => mainContainer.Clock = new FramedOffsetClock(Clock, false) { Offset = 3600.0 * 24 * 1000 * 98 }); } - [Test] - public void TestHoverBackground() - { - Box hoverBackground = null; - - AddStep("Retrieve hover background", () => hoverBackground = (Box)toolbarClock.Children[0]); - - AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(toolbarClock, new Vector2(0, 200))); - AddAssert("Hover background is not visible", () => hoverBackground.Alpha == 0); - AddStep("Move mouse on top of clock", () => InputManager.MoveMouseTo(toolbarClock)); - AddAssert("Hover background is visible", () => hoverBackground.Alpha != 0); - AddStep("Move mouse away from clock", () => InputManager.MoveMouseTo(toolbarClock, new Vector2(0, 200))); - AddUntilStep("Hover background is not visible", () => hoverBackground.Alpha == 0); - } - [Test] public void TestDisplayModeChange() { From a1baced7774603a400e74c191ec6f3811a66f728 Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Sat, 2 Apr 2022 04:45:21 +0200 Subject: [PATCH 241/285] Change behavior of the display mode test I remembered to run InspectCode this time, all good --- osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs index 269b3cbad0..2cf73f5442 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs @@ -92,12 +92,10 @@ namespace osu.Game.Tests.Visual.Menus [Test] public void TestDisplayModeChange() { - ToolbarClockDisplayMode initialDisplayMode = 0; - - AddStep("Retrieve current state", () => initialDisplayMode = clockDisplayMode.Value); + AddStep("Set clock display mode", () => clockDisplayMode.Value = ToolbarClockDisplayMode.Full); AddStep("Trigger click", () => toolbarClock.TriggerClick()); - AddAssert("State changed from initial", () => clockDisplayMode.Value != initialDisplayMode); + AddAssert("State is digital with runtime", () => clockDisplayMode.Value == ToolbarClockDisplayMode.DigitalWithRuntime); AddStep("Trigger click", () => toolbarClock.TriggerClick()); AddAssert("State changed from initial", () => clockDisplayMode.Value != initialDisplayMode); AddStep("Trigger click", () => toolbarClock.TriggerClick()); From c103cee1d5dce8e5b157f594ebc9c9af0825363e Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Sat, 2 Apr 2022 04:53:47 +0200 Subject: [PATCH 242/285] Need to commit the second half again because my Git UI messed up --- osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs index 2cf73f5442..87d836687f 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarClock.cs @@ -97,11 +97,11 @@ namespace osu.Game.Tests.Visual.Menus AddStep("Trigger click", () => toolbarClock.TriggerClick()); AddAssert("State is digital with runtime", () => clockDisplayMode.Value == ToolbarClockDisplayMode.DigitalWithRuntime); AddStep("Trigger click", () => toolbarClock.TriggerClick()); - AddAssert("State changed from initial", () => clockDisplayMode.Value != initialDisplayMode); + AddAssert("State is digital", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Digital); AddStep("Trigger click", () => toolbarClock.TriggerClick()); - AddAssert("State changed from initial", () => clockDisplayMode.Value != initialDisplayMode); + AddAssert("State is analog", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Analog); AddStep("Trigger click", () => toolbarClock.TriggerClick()); - AddAssert("State is equal to initial", () => clockDisplayMode.Value == initialDisplayMode); + AddAssert("State is full", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Full); } } } From a252c4cad57cae5472adb69101e288e46ab2c18c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Apr 2022 18:08:23 +0300 Subject: [PATCH 243/285] Add random pass/play count data in test scene --- .../Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs index be3fc7aff9..82b34c50c2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs @@ -71,7 +71,9 @@ namespace osu.Game.Tests.Visual.Online { Fails = Enumerable.Range(1, 100).Select(_ => RNG.Next(10)).ToArray(), Retries = Enumerable.Range(-2, 100).Select(_ => RNG.Next(10)).ToArray(), - } + }, + PassCount = RNG.Next(0, 999), + PlayCount = RNG.Next(1000, 1999), }; } From ced5be33e6ac062126ba7352c31eddf00683f303 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Apr 2022 18:08:57 +0300 Subject: [PATCH 244/285] Display pass/play count in success rate percentage tooltip --- osu.Game/Overlays/BeatmapSet/SuccessRate.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/SuccessRate.cs b/osu.Game/Overlays/BeatmapSet/SuccessRate.cs index e08f099226..fed3d7ddaa 100644 --- a/osu.Game/Overlays/BeatmapSet/SuccessRate.cs +++ b/osu.Game/Overlays/BeatmapSet/SuccessRate.cs @@ -5,6 +5,8 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -19,7 +21,7 @@ namespace osu.Game.Overlays.BeatmapSet protected readonly FailRetryGraph Graph; private readonly FillFlowContainer header; - private readonly OsuSpriteText successPercent; + private readonly SuccessRatePercentage successPercent; private readonly Bar successRate; private readonly Container percentContainer; @@ -45,6 +47,7 @@ namespace osu.Game.Overlays.BeatmapSet float rate = playCount != 0 ? (float)passCount / playCount : 0; successPercent.Text = rate.ToLocalisableString(@"0.#%"); + successPercent.TooltipText = $"{passCount} / {playCount}"; successRate.Length = rate; percentContainer.ResizeWidthTo(successRate.Length, 250, Easing.InOutCubic); @@ -80,7 +83,7 @@ namespace osu.Game.Overlays.BeatmapSet RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Width = 0f, - Child = successPercent = new OsuSpriteText + Child = successPercent = new SuccessRatePercentage { Anchor = Anchor.TopRight, Origin = Anchor.TopCentre, @@ -121,5 +124,10 @@ namespace osu.Game.Overlays.BeatmapSet Graph.Padding = new MarginPadding { Top = header.DrawHeight }; } + + private class SuccessRatePercentage : OsuSpriteText, IHasTooltip + { + public LocalisableString TooltipText { get; set; } + } } } From c4635f3c03f1ef7aac5a153459d0b55d8e068259 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Apr 2022 18:36:32 +0300 Subject: [PATCH 245/285] Add failing test case --- .../Online/TestSceneBeatmapListingOverlay.cs | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index a056e0cd2c..159c49ebea 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -5,9 +5,11 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -29,6 +31,14 @@ namespace osu.Game.Tests.Visual.Online private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType().Single(); + private OsuConfigManager localConfig; + + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage)); + } + [SetUpSteps] public void SetUpSteps() { @@ -61,6 +71,8 @@ namespace osu.Game.Tests.Visual.Online Id = API.LocalUser.Value.Id + 1, }; }); + + AddStep("reset size", () => localConfig.SetValue(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal)); } [Test] @@ -120,24 +132,25 @@ namespace osu.Game.Tests.Visual.Online assertAllCardsOfType(30); } - [Test] - public void TestCardSizeSwitching() + [TestCase(false)] + [TestCase(true)] + public void TestCardSizeSwitching(bool viaConfig) { AddAssert("is visible", () => overlay.State.Value == Visibility.Visible); AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 100).ToArray())); assertAllCardsOfType(100); - setCardSize(BeatmapCardSize.Extra); + setCardSize(BeatmapCardSize.Extra, viaConfig); assertAllCardsOfType(100); - setCardSize(BeatmapCardSize.Normal); + setCardSize(BeatmapCardSize.Normal, viaConfig); assertAllCardsOfType(100); AddStep("fetch for 0 beatmaps", () => fetchFor()); AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); - setCardSize(BeatmapCardSize.Extra); + setCardSize(BeatmapCardSize.Extra, viaConfig); AddAssert("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); } @@ -361,7 +374,13 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep("\"no maps found\" placeholder not shown", () => !overlay.ChildrenOfType().Any(d => d.IsPresent)); } - private void setCardSize(BeatmapCardSize cardSize) => AddStep($"set card size to {cardSize}", () => overlay.ChildrenOfType().Single().Current.Value = cardSize); + private void setCardSize(BeatmapCardSize cardSize, bool viaConfig) => AddStep($"set card size to {cardSize}", () => + { + if (!viaConfig) + overlay.ChildrenOfType().Single().Current.Value = cardSize; + else + localConfig.SetValue(OsuSetting.BeatmapListingCardSize, cardSize); + }); private void assertAllCardsOfType(int expectedCount) where T : BeatmapCard => @@ -370,5 +389,11 @@ namespace osu.Game.Tests.Visual.Online int loadedCorrectCount = this.ChildrenOfType().Count(card => card.IsLoaded && card.GetType() == typeof(T)); return loadedCorrectCount > 0 && loadedCorrectCount == expectedCount; }); + + protected override void Dispose(bool isDisposing) + { + localConfig?.Dispose(); + base.Dispose(isDisposing); + } } } From beb8426d3b866743fdaebf84e82d2e518bc3769c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Apr 2022 18:36:49 +0300 Subject: [PATCH 246/285] Save beatmap listing card size to game config --- osu.Game/Configuration/OsuConfigManager.cs | 4 ++++ .../BeatmapListing/BeatmapListingFilterControl.cs | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index e8f13ba902..2f966ac0a9 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -10,6 +10,7 @@ using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -44,6 +45,8 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f); + SetDefault(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal); + SetDefault(OsuSetting.ToolbarClockDisplayMode, ToolbarClockDisplayMode.Full); // Online settings @@ -297,6 +300,7 @@ namespace osu.Game.Configuration RandomSelectAlgorithm, ShowFpsDisplay, ChatDisplayHeight, + BeatmapListingCardSize, ToolbarClockDisplayMode, Version, ShowConvertedBeatmaps, diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 0f87f04270..e4628e3723 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Framework.Threading; using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Configuration; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -53,7 +54,9 @@ namespace osu.Game.Overlays.BeatmapListing /// /// The currently selected . /// - public IBindable CardSize { get; } = new Bindable(); + public IBindable CardSize => cardSize; + + private readonly Bindable cardSize = new Bindable(); private readonly BeatmapListingSearchControl searchControl; private readonly BeatmapListingSortTabControl sortControl; @@ -128,6 +131,9 @@ namespace osu.Game.Overlays.BeatmapListing }; } + [Resolved] + private OsuConfigManager config { get; set; } + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, IAPIProvider api) { @@ -141,6 +147,8 @@ namespace osu.Game.Overlays.BeatmapListing { base.LoadComplete(); + config.BindWith(OsuSetting.BeatmapListingCardSize, cardSize); + var sortCriteria = sortControl.Current; var sortDirection = sortControl.SortDirection; From b9421d1415af7e15c07b3f7237603170ea4a7a92 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Sat, 2 Apr 2022 17:14:27 +0100 Subject: [PATCH 247/285] Simplify text clear and placeholder change in `ChatTextBox` --- osu.Game/Overlays/Chat/ChatTextBox.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatTextBox.cs b/osu.Game/Overlays/Chat/ChatTextBox.cs index 35ed26cda3..e0f949caba 100644 --- a/osu.Game/Overlays/Chat/ChatTextBox.cs +++ b/osu.Game/Overlays/Chat/ChatTextBox.cs @@ -20,8 +20,10 @@ namespace osu.Game.Overlays.Chat ShowSearch.BindValueChanged(change => { - PlaceholderText = change.NewValue ? "type here to search" : "type here"; - Schedule(() => Text = string.Empty); + bool showSearch = change.NewValue; + + PlaceholderText = showSearch ? "type here to search" : "type here"; + Text = string.Empty; }, true); } From 2297073b7eec019a1553735e7fd846f9488aad34 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Sat, 2 Apr 2022 17:15:19 +0100 Subject: [PATCH 248/285] Use `OnChatMessageCommit` & `OnSearchTermsChanged` events in `ChatTextBar` --- .../Visual/Online/TestSceneChatTextBox.cs | 39 ++++++++++++++++--- osu.Game/Overlays/Chat/ChatTextBar.cs | 31 +++++++++++++-- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs index e72a1d6652..982fbc397d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs @@ -25,6 +25,7 @@ namespace osu.Game.Tests.Visual.Online private readonly Bindable currentChannel = new Bindable(); private OsuSpriteText commitText; + private OsuSpriteText searchText; private ChatTextBar bar; [SetUp] @@ -47,11 +48,32 @@ namespace osu.Game.Tests.Visual.Online { new Drawable[] { - commitText = new OsuSpriteText + new GridContainer { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Font = OsuFont.Default.With(size: 20), + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(), + }, + Content = new[] + { + new Drawable[] + { + commitText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Default.With(size: 20), + }, + searchText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Default.With(size: 20), + }, + }, + }, }, }, new Drawable[] @@ -66,12 +88,17 @@ namespace osu.Game.Tests.Visual.Online }, }; - bar.TextBox.OnCommit += (sender, newText) => + bar.OnChatMessageCommit += (sender, newText) => { - commitText.Text = $"Commit: {sender.Text}"; + commitText.Text = $"OnChatMessageCommit: {sender.Text}"; commitText.FadeOutFromOne(1000, Easing.InQuint); sender.Text = string.Empty; }; + + bar.OnSearchTermsChanged += (text) => + { + searchText.Text = $"OnSearchTermsChanged: {text}"; + }; }); } diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index d7edbb83b6..51c74ee0a5 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -3,15 +3,15 @@ #nullable enable +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; +using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osuTK; @@ -21,13 +21,16 @@ namespace osu.Game.Overlays.Chat { public readonly BindableBool ShowSearch = new BindableBool(); - public ChatTextBox TextBox { get; private set; } = null!; + public event TextBox.OnCommitHandler? OnChatMessageCommit; + + public event Action? OnSearchTermsChanged; [Resolved] private Bindable currentChannel { get; set; } = null!; private OsuTextFlowContainer chattingTextContainer = null!; private Container searchIconContainer = null!; + private ChatTextBox chatTextBox = null!; private const float chatting_text_width = 180; private const float search_icon_width = 40; @@ -86,7 +89,7 @@ namespace osu.Game.Overlays.Chat { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Right = 5 }, - Child = TextBox = new ChatTextBox + Child = chatTextBox = new ChatTextBox { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -106,12 +109,20 @@ namespace osu.Game.Overlays.Chat { base.LoadComplete(); + chatTextBox.Current.ValueChanged += chatTextBoxChange; + chatTextBox.OnCommit += chatTextBoxCommit; + ShowSearch.BindValueChanged(change => { bool showSearch = change.NewValue; chattingTextContainer.FadeTo(showSearch ? 0 : 1); searchIconContainer.FadeTo(showSearch ? 1 : 0); + + // Clear search terms if any exist when switching back to chat mode + if (!showSearch) + OnSearchTermsChanged?.Invoke(string.Empty); + }, true); currentChannel.BindValueChanged(change => @@ -134,5 +145,17 @@ namespace osu.Game.Overlays.Chat } }, true); } + + private void chatTextBoxChange(ValueChangedEvent change) + { + if (ShowSearch.Value) + OnSearchTermsChanged?.Invoke(change.NewValue); + } + + private void chatTextBoxCommit(TextBox sender, bool newText) + { + if (!ShowSearch.Value) + OnChatMessageCommit?.Invoke(sender, newText); + } } } From 8534dd34632508c05edb0fd6132aca68023c845f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Apr 2022 19:24:16 +0300 Subject: [PATCH 249/285] Simplify `TestCase` attributes to one `Values` attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Visual/Online/TestSceneBeatmapListingOverlay.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 159c49ebea..9a7bd17902 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -132,9 +132,8 @@ namespace osu.Game.Tests.Visual.Online assertAllCardsOfType(30); } - [TestCase(false)] - [TestCase(true)] - public void TestCardSizeSwitching(bool viaConfig) + [Test] + public void TestCardSizeSwitching([Values] bool viaConfig) { AddAssert("is visible", () => overlay.State.Value == Visibility.Visible); From 9e152cd3fd8aa48354e8bc34bfe3589d2ba77ad4 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Sat, 2 Apr 2022 17:27:44 +0100 Subject: [PATCH 250/285] Fix code quality issues --- osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs | 2 +- osu.Game/Overlays/Chat/ChatTextBar.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs index 982fbc397d..42d4a8efbd 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Online sender.Text = string.Empty; }; - bar.OnSearchTermsChanged += (text) => + bar.OnSearchTermsChanged += text => { searchText.Text = $"OnSearchTermsChanged: {text}"; }; diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 51c74ee0a5..0eb8056d76 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -122,7 +122,6 @@ namespace osu.Game.Overlays.Chat // Clear search terms if any exist when switching back to chat mode if (!showSearch) OnSearchTermsChanged?.Invoke(string.Empty); - }, true); currentChannel.BindValueChanged(change => From b815f685fc67d869fa99ebd65a8ae49972122a22 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Apr 2022 19:28:33 +0300 Subject: [PATCH 251/285] Flip `viaConfig` conditional branch --- .../Visual/Online/TestSceneBeatmapListingOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 9a7bd17902..5999125013 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -375,10 +375,10 @@ namespace osu.Game.Tests.Visual.Online private void setCardSize(BeatmapCardSize cardSize, bool viaConfig) => AddStep($"set card size to {cardSize}", () => { - if (!viaConfig) - overlay.ChildrenOfType().Single().Current.Value = cardSize; - else + if (viaConfig) localConfig.SetValue(OsuSetting.BeatmapListingCardSize, cardSize); + else + overlay.ChildrenOfType().Single().Current.Value = cardSize; }); private void assertAllCardsOfType(int expectedCount) From 0f9461689085cb790de5a3850d051a344d3f0fc2 Mon Sep 17 00:00:00 2001 From: Ame Date: Sat, 2 Apr 2022 19:41:15 +0200 Subject: [PATCH 252/285] Make overlay shortcuts able to be toggled instead of repeatable --- osu.Game/OsuGame.cs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 73121f6e7d..3703318e81 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -95,6 +95,8 @@ namespace osu.Game private SkinEditorOverlay skinEditor; + private NowPlayingOverlay nowPlayingOverlay; + private Container overlayContent; private Container rightFloatingOverlayContent; @@ -818,7 +820,7 @@ namespace osu.Game Origin = Anchor.TopRight, }, rightFloatingOverlayContent.Add, true); - loadComponentSingleFile(new NowPlayingOverlay + loadComponentSingleFile(nowPlayingOverlay = new NowPlayingOverlay { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -1069,6 +1071,26 @@ namespace osu.Game SkinManager.SelectRandomSkin(); return true; + + case GlobalAction.ToggleChat: + chatOverlay.ToggleVisibility(); + return true; + + case GlobalAction.ToggleSocial: + dashboard.ToggleVisibility(); + return true; + + case GlobalAction.ToggleNowPlaying: + nowPlayingOverlay.ToggleVisibility(); + return true; + + case GlobalAction.ToggleBeatmapListing: + beatmapListing.ToggleVisibility(); + return true; + + case GlobalAction.ToggleSettings: + Settings.ToggleVisibility(); + return true; } return false; From 35629e9be815c41788633363c38ec9f6450669ea Mon Sep 17 00:00:00 2001 From: Ame Date: Sat, 2 Apr 2022 20:45:00 +0200 Subject: [PATCH 253/285] General fix for ToolbarButton toggle repetition --- osu.Game/OsuGame.cs | 24 +--------------------- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 2 +- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3703318e81..73121f6e7d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -95,8 +95,6 @@ namespace osu.Game private SkinEditorOverlay skinEditor; - private NowPlayingOverlay nowPlayingOverlay; - private Container overlayContent; private Container rightFloatingOverlayContent; @@ -820,7 +818,7 @@ namespace osu.Game Origin = Anchor.TopRight, }, rightFloatingOverlayContent.Add, true); - loadComponentSingleFile(nowPlayingOverlay = new NowPlayingOverlay + loadComponentSingleFile(new NowPlayingOverlay { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -1071,26 +1069,6 @@ namespace osu.Game SkinManager.SelectRandomSkin(); return true; - - case GlobalAction.ToggleChat: - chatOverlay.ToggleVisibility(); - return true; - - case GlobalAction.ToggleSocial: - dashboard.ToggleVisibility(); - return true; - - case GlobalAction.ToggleNowPlaying: - nowPlayingOverlay.ToggleVisibility(); - return true; - - case GlobalAction.ToggleBeatmapListing: - beatmapListing.ToggleVisibility(); - return true; - - case GlobalAction.ToggleSettings: - Settings.ToggleVisibility(); - return true; } return false; diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index c855b76680..670ce65c6b 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -190,7 +190,7 @@ namespace osu.Game.Overlays.Toolbar public bool OnPressed(KeyBindingPressEvent e) { - if (e.Action == Hotkey) + if (!e.Repeat && e.Action == Hotkey) { TriggerClick(); return true; From 01b10e68d2fc8de98912f715bb5e02599d4ba9a9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 2 Apr 2022 23:47:20 +0300 Subject: [PATCH 254/285] Adjust taiko hit objects sizes to match osu!(stable) --- osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index f047c03f4b..1a1fde1990 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Objects /// /// Default size of a drawable taiko hit object. /// - public const float DEFAULT_SIZE = 0.45f; + public const float DEFAULT_SIZE = 0.475f; public override Judgement CreateJudgement() => new TaikoJudgement(); diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs index 6c17573b50..43a099b900 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Objects /// /// Scale multiplier for a strong drawable taiko hit object. /// - public const float STRONG_SCALE = 1.4f; + public const float STRONG_SCALE = 1.525f; /// /// Default size of a strong drawable taiko hit object. From 94fa5e2ef20ab519484638d34be6bbb5238a256c Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Sat, 2 Apr 2022 21:58:54 +0100 Subject: [PATCH 255/285] Use `Action` for event `OnChatMessageCommitted` & clear textbox internally --- osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs | 5 ++--- osu.Game/Overlays/Chat/ChatTextBar.cs | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs index 42d4a8efbd..e5dd492183 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs @@ -88,11 +88,10 @@ namespace osu.Game.Tests.Visual.Online }, }; - bar.OnChatMessageCommit += (sender, newText) => + bar.OnChatMessageCommitted += text => { - commitText.Text = $"OnChatMessageCommit: {sender.Text}"; + commitText.Text = $"OnChatMessageCommitted: {text}"; commitText.FadeOutFromOne(1000, Easing.InQuint); - sender.Text = string.Empty; }; bar.OnSearchTermsChanged += text => diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 0eb8056d76..531c417abc 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Chat { public readonly BindableBool ShowSearch = new BindableBool(); - public event TextBox.OnCommitHandler? OnChatMessageCommit; + public event Action? OnChatMessageCommitted; public event Action? OnSearchTermsChanged; @@ -154,7 +154,9 @@ namespace osu.Game.Overlays.Chat private void chatTextBoxCommit(TextBox sender, bool newText) { if (!ShowSearch.Value) - OnChatMessageCommit?.Invoke(sender, newText); + OnChatMessageCommitted?.Invoke(sender.Text); + + sender.Text = string.Empty; } } } From 4ce69890d4fbad0f7990fe6ceb645cf05ae5252a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 3 Apr 2022 00:21:23 +0300 Subject: [PATCH 256/285] Use `TaikoHitObject.DEFAULT_SIZE` for default circle piece symbol size --- osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs index a106c4f629..f2452ad88c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Taiko.Objects; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning.Default @@ -24,8 +25,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default /// public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour { - public const float SYMBOL_SIZE = 0.45f; + public const float SYMBOL_SIZE = TaikoHitObject.DEFAULT_SIZE; public const float SYMBOL_BORDER = 8; + private const double pre_beat_transition_time = 80; private Color4 accentColour; From 534cc18ff9ff6d1a8f5726bef122a3ff4c79c006 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 3 Apr 2022 01:21:48 +0300 Subject: [PATCH 257/285] Adjust osu!taiko legacy hit target size to match osu!(stable) --- .../Skinning/Legacy/TaikoLegacyHitTarget.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs index 9feb2054da..c4657fcc49 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy new Sprite { Texture = skin.GetTexture("approachcircle"), - Scale = new Vector2(0.73f), + Scale = new Vector2(0.83f), Alpha = 0.47f, // eyeballed to match stable Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy new Sprite { Texture = skin.GetTexture("taikobigcircle"), - Scale = new Vector2(0.7f), + Scale = new Vector2(0.8f), Alpha = 0.22f, // eyeballed to match stable Anchor = Anchor.Centre, Origin = Anchor.Centre, From 970b1951ace07653526d5633f7975019ba006b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 3 Apr 2022 14:23:45 +0200 Subject: [PATCH 258/285] Rewrite logic slightly to better convey meaning of textbox clear --- osu.Game/Overlays/Chat/ChatTextBar.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 531c417abc..ef20149dac 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -153,9 +153,10 @@ namespace osu.Game.Overlays.Chat private void chatTextBoxCommit(TextBox sender, bool newText) { - if (!ShowSearch.Value) - OnChatMessageCommitted?.Invoke(sender.Text); + if (ShowSearch.Value) + return; + OnChatMessageCommitted?.Invoke(sender.Text); sender.Text = string.Empty; } } From 6d1844adc3a9b01b8d33a07bdf56fcdc6d671c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 3 Apr 2022 14:27:37 +0200 Subject: [PATCH 259/285] Use `nameof()` in test to reference event names --- osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs index e5dd492183..a241aa0517 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatTextBox.cs @@ -90,13 +90,13 @@ namespace osu.Game.Tests.Visual.Online bar.OnChatMessageCommitted += text => { - commitText.Text = $"OnChatMessageCommitted: {text}"; + commitText.Text = $"{nameof(bar.OnChatMessageCommitted)}: {text}"; commitText.FadeOutFromOne(1000, Easing.InQuint); }; bar.OnSearchTermsChanged += text => { - searchText.Text = $"OnSearchTermsChanged: {text}"; + searchText.Text = $"{nameof(bar.OnSearchTermsChanged)}: {text}"; }; }); } From 1393e3628be595cc9f2ab67cc7ee92d453e2acf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 3 Apr 2022 15:24:00 +0200 Subject: [PATCH 260/285] Invert operands for better readability --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 670ce65c6b..4a839b048c 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -190,7 +190,7 @@ namespace osu.Game.Overlays.Toolbar public bool OnPressed(KeyBindingPressEvent e) { - if (!e.Repeat && e.Action == Hotkey) + if (e.Action == Hotkey && !e.Repeat) { TriggerClick(); return true; From e1f147a207a42f47370ac42527226212e23d3f9c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 4 Apr 2022 13:46:41 +0900 Subject: [PATCH 261/285] Mutate playlist in EditUserPlaylistItem --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 0efaf16f99..4a974cf61d 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -458,8 +458,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public async Task EditUserPlaylistItem(int userId, MultiplayerPlaylistItem item) { Debug.Assert(Room != null); - Debug.Assert(APIRoom != null); Debug.Assert(currentItem != null); + Debug.Assert(serverSideAPIRoom != null); item.OwnerID = userId; @@ -478,6 +478,7 @@ namespace osu.Game.Tests.Visual.Multiplayer item.PlaylistOrder = existingItem.PlaylistOrder; serverSidePlaylist[serverSidePlaylist.IndexOf(existingItem)] = item; + serverSideAPIRoom.Playlist[serverSideAPIRoom.Playlist.IndexOf(serverSideAPIRoom.Playlist.Single(i => i.ID == item.ID))] = new PlaylistItem(item); await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); } From 39c6eed81969fe883264c04610c4b05ea0968816 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Apr 2022 14:10:57 +0900 Subject: [PATCH 262/285] Update resources --- 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 6a3b113fa2..200008017f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3c01f29671..18faf318a6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index c8f170497d..b0c382c695 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -62,7 +62,7 @@ - + From 0abebe4d23a17cfd772fe1b700ddefb2a2427a57 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Apr 2022 14:36:03 +0900 Subject: [PATCH 263/285] Stabilise countdown updates to be based on when whole seconds change --- .../Match/MultiplayerReadyButton.cs | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 62be9ad3bd..a20d250ea8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -47,17 +47,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match countdownChangeTime = DateTimeOffset.Now; } + scheduleNextCountdownUpdate(); + + updateButtonText(); + updateButtonColour(); + }); + + private void scheduleNextCountdownUpdate() + { if (countdown != null) - countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 100, true); + { + // If a countdown is active, schedule relevant components to update on the next whole second. + double timeToNextSecond = countdownTimeRemaining.TotalMilliseconds % 1000; + + countdownUpdateDelegate = Scheduler.AddDelayed(onCountdownTick, timeToNextSecond); + } else { countdownUpdateDelegate?.Cancel(); countdownUpdateDelegate = null; } - updateButtonText(); - updateButtonColour(); - }); + void onCountdownTick() + { + updateButtonText(); + scheduleNextCountdownUpdate(); + } + } private void updateButtonText() { @@ -75,15 +91,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (countdown != null) { - TimeSpan timeElapsed = DateTimeOffset.Now - countdownChangeTime; - TimeSpan countdownRemaining; - - if (timeElapsed > countdown.TimeRemaining) - countdownRemaining = TimeSpan.Zero; - else - countdownRemaining = countdown.TimeRemaining - timeElapsed; - - string countdownText = $"Starting in {countdownRemaining:mm\\:ss}"; + string countdownText = $"Starting in {countdownTimeRemaining:mm\\:ss}"; switch (localUser?.State) { @@ -116,6 +124,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } } + private TimeSpan countdownTimeRemaining + { + get + { + TimeSpan timeElapsed = DateTimeOffset.Now - countdownChangeTime; + TimeSpan remaining; + + if (timeElapsed > countdown.TimeRemaining) + remaining = TimeSpan.Zero; + else + remaining = countdown.TimeRemaining - timeElapsed; + + return remaining; + } + } + private void updateButtonColour() { if (room == null) From 09e15f5496a8ecee0f0c77f3743a3b664a150da9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Apr 2022 20:27:03 +0900 Subject: [PATCH 264/285] Remove nullable on `RealmBackedResourceStore` realm parameter --- osu.Game/Skinning/RealmBackedResourceStore.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index e727a7e59a..c81e976a67 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -24,7 +24,7 @@ namespace osu.Game.Skinning private readonly Live liveSource; private readonly IDisposable? realmSubscription; - public RealmBackedResourceStore(Live source, IResourceStore underlyingStore, RealmAccess? realm) + public RealmBackedResourceStore(Live source, IResourceStore underlyingStore, RealmAccess realm) : base(underlyingStore) { liveSource = source; @@ -32,7 +32,7 @@ namespace osu.Game.Skinning invalidateCache(); Debug.Assert(fileToStoragePathMapping != null); - realmSubscription = realm?.RegisterForNotifications(r => r.All().Where(s => s.ID == source.ID), skinChanged); + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.ID == source.ID), skinChanged); } protected override void Dispose(bool disposing) From 300feadf6a9adba8120db8dc3e50fcef5c33c99d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Apr 2022 20:27:46 +0900 Subject: [PATCH 265/285] Update `SkinnableSprite` to match more broad usage --- osu.Game/Skinning/SkinnableSprite.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index c6cc4c1bdd..e7c62302b1 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Skinning { /// - /// A skinnable element which uses a stable sprite and can therefore share implementation logic. + /// A skinnable element which uses a single texture backing. /// public class SkinnableSprite : SkinnableDrawable, ISkinnableDrawable { From dac5dfde8f8cee98be41887a5b4c94444efe3953 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Apr 2022 20:28:43 +0900 Subject: [PATCH 266/285] Remove unnecessary `LazyThreadSafetyMode` specification --- osu.Game/Skinning/RealmBackedResourceStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index c81e976a67..0353b8a64d 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -61,7 +61,7 @@ namespace osu.Game.Skinning return null; } - private void invalidateCache() => fileToStoragePathMapping = new Lazy>(initialiseFileCache, LazyThreadSafetyMode.ExecutionAndPublication); + private void invalidateCache() => fileToStoragePathMapping = new Lazy>(initialiseFileCache); private Dictionary initialiseFileCache() => liveSource.PerformRead(source => { From de30a42558b7ab5d0e03cae2ed80d574cb694732 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Apr 2022 20:30:14 +0900 Subject: [PATCH 267/285] Add `region` for import methods and move `Dispose` to end of time --- osu.Game/Skinning/Editor/SkinEditor.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index df0bb7a70c..607a881d28 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -197,13 +197,6 @@ namespace osu.Game.Skinning.Editor SelectedComponents.BindCollectionChanged((_, __) => Scheduler.AddOnce(populateSettings), true); } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - game?.UnregisterImportHandler(this); - } - public void UpdateTargetScreen(Drawable targetScreen) { this.targetScreen = targetScreen; @@ -337,6 +330,8 @@ namespace osu.Game.Skinning.Editor availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item); } + #region Drag & drop import handling + public Task Import(params string[] paths) { Schedule(() => @@ -368,5 +363,14 @@ namespace osu.Game.Skinning.Editor public Task Import(params ImportTask[] tasks) => throw new NotImplementedException(); public IEnumerable HandledExtensions => new[] { ".jpg", ".jpeg", ".png" }; + + #endregion + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + game?.UnregisterImportHandler(this); + } } } From 8185020f128dcda5d5a1feb236b02e7a3fbd51d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Apr 2022 20:35:48 +0900 Subject: [PATCH 268/285] Improve the visual of the missing sprite sprite --- osu.Game/Skinning/RealmBackedResourceStore.cs | 1 - osu.Game/Skinning/SkinnableSprite.cs | 33 +++++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index 0353b8a64d..0057132044 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading; using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Game.Database; diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index e7c62302b1..c5f110f908 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -6,9 +6,11 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Configuration; +using osu.Game.Graphics.Sprites; using osu.Game.Overlays.Settings; using osuTK; @@ -55,13 +57,7 @@ namespace osu.Game.Skinning var texture = textures.Get(component.LookupName); if (texture == null) - { - return new SpriteIcon - { - Size = new Vector2(100), - Icon = FontAwesome.Solid.QuestionCircle - }; - } + return new SpriteNotFound(component.LookupName); return new Sprite { Texture = texture }; } @@ -98,5 +94,28 @@ namespace osu.Game.Skinning Items = availableFiles; } } + + public class SpriteNotFound : CompositeDrawable + { + public SpriteNotFound(string lookup) + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new SpriteIcon + { + Size = new Vector2(50), + Icon = FontAwesome.Solid.QuestionCircle + }, + new OsuSpriteText + { + Position = new Vector2(25, 50), + Text = $"missing: {lookup}", + Origin = Anchor.TopCentre, + } + }; + } + } } } From 5f358a04e98ba580ce6fa7b7c32ee23a35d8cba3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Apr 2022 20:40:19 +0900 Subject: [PATCH 269/285] Return a valid "lighting" response from `DefaultSkin` This is temporary to allow the new sprite lookup flow to potentially be merged before hit lighting skinnability is addressed. --- osu.Game/Skinning/DefaultSkin.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index c645b0fae4..119b0ec9ad 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -158,6 +158,13 @@ namespace osu.Game.Skinning break; } + switch (component.LookupName) + { + // Temporary until default skin has a valid hit lighting. + case @"lighting": + return Drawable.Empty(); + } + if (GetTexture(component.LookupName) is Texture t) return new Sprite { Texture = t }; From 6b5ee6d89dbf64de2003f0b8cb9e77da75def66b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Apr 2022 20:44:05 +0900 Subject: [PATCH 270/285] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 6a3b113fa2..ff14c97cd9 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3c01f29671..7b0f8c72c5 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index c8f170497d..88daf2eda7 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - + From 3708f2b744b46251c3d680b27ce7583109f3f2b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Apr 2022 01:05:04 +0900 Subject: [PATCH 271/285] Update resources --- 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 3203ffeac3..fbe13b11ee 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7e193a37f9..1bebf78d97 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index a68bebeabe..efd5bac38e 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -62,7 +62,7 @@ - + From 9a07a95d39817ce408487253e410d845ee11c27d Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Mon, 4 Apr 2022 19:22:53 +0200 Subject: [PATCH 272/285] Make several delete confirmation buttons dangerous buttons Includes: - Mass deletion - Beatmap deletion - Local score deletion --- .../Sections/Maintenance/MassDeleteConfirmationDialog.cs | 2 +- osu.Game/Screens/Select/BeatmapDeleteDialog.cs | 2 +- osu.Game/Screens/Select/LocalScoreDeleteDialog.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs index 6380232bbb..c481c80d82 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs @@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance HeaderText = @"Confirm deletion of"; Buttons = new PopupDialogButton[] { - new PopupDialogOkButton + new PopupDialogDangerousButton { Text = @"Yes. Go for it.", Action = deleteAction diff --git a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs index 1ac278d045..b156c2485b 100644 --- a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs +++ b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Select HeaderText = @"Confirm deletion of"; Buttons = new PopupDialogButton[] { - new PopupDialogOkButton + new PopupDialogDangerousButton { Text = @"Yes. Totally. Delete it.", Action = () => manager?.Delete(beatmap), diff --git a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs index 1ae244281b..cb96e3f23e 100644 --- a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs +++ b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Select HeaderText = "Confirm deletion of local score"; Buttons = new PopupDialogButton[] { - new PopupDialogOkButton + new PopupDialogDangerousButton { Text = "Yes. Please.", Action = () => scoreManager?.Delete(score) From a1ded66fd85bcc09e2cfe32cf2ca3d938726b2c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Apr 2022 21:59:09 +0200 Subject: [PATCH 273/285] Fix various breakage in delete local score test scene --- .../TestSceneDeleteLocalScore.cs | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index a0a1feff36..c8a8fd43fb 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -44,9 +44,6 @@ namespace osu.Game.Tests.Visual.UserInterface private BeatmapInfo beatmapInfo; - [Resolved] - private RealmAccess realm { get; set; } - [Cached] private readonly DialogOverlay dialogOverlay; @@ -92,6 +89,12 @@ namespace osu.Game.Tests.Visual.UserInterface dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Realm, Scheduler)); Dependencies.Cache(Realm); + return dependencies; + } + + [BackgroundDependencyLoader] + private void load() => Schedule(() => + { var imported = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely(); imported?.PerformRead(s => @@ -115,26 +118,26 @@ namespace osu.Game.Tests.Visual.UserInterface importedScores.Add(scoreManager.Import(score).Value); } }); - - return dependencies; - } - - [SetUp] - public void Setup() => Schedule(() => - { - realm.Run(r => - { - // Due to soft deletions, we can re-use deleted scores between test runs - scoreManager.Undelete(r.All().Where(s => s.DeletePending).ToList()); - }); - - leaderboard.BeatmapInfo = beatmapInfo; - leaderboard.RefetchScores(); // Required in the case that the beatmap hasn't changed }); [SetUpSteps] public void SetupSteps() { + AddUntilStep("ensure scores imported", () => importedScores.Count == 50); + AddStep("undelete scores", () => + { + Realm.Run(r => + { + // Due to soft deletions, we can re-use deleted scores between test runs + scoreManager.Undelete(r.All().Where(s => s.DeletePending).ToList()); + }); + }); + AddStep("set up leaderboard", () => + { + leaderboard.BeatmapInfo = beatmapInfo; + leaderboard.RefetchScores(); // Required in the case that the beatmap hasn't changed + }); + // Ensure the leaderboard items have finished showing up AddStep("finish transforms", () => leaderboard.FinishTransforms(true)); AddUntilStep("wait for drawables", () => leaderboard.ChildrenOfType().Any()); From f73062a0d6c141290c93f227db7dd63173175324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Apr 2022 22:22:55 +0200 Subject: [PATCH 274/285] Revert "Remove nullable on `RealmBackedResourceStore` realm parameter" This reverts commit 09e15f5496a8ecee0f0c77f3743a3b664a150da9. --- osu.Game/Skinning/RealmBackedResourceStore.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index 0057132044..7fa24284ee 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -23,7 +23,7 @@ namespace osu.Game.Skinning private readonly Live liveSource; private readonly IDisposable? realmSubscription; - public RealmBackedResourceStore(Live source, IResourceStore underlyingStore, RealmAccess realm) + public RealmBackedResourceStore(Live source, IResourceStore underlyingStore, RealmAccess? realm) : base(underlyingStore) { liveSource = source; @@ -31,7 +31,7 @@ namespace osu.Game.Skinning invalidateCache(); Debug.Assert(fileToStoragePathMapping != null); - realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.ID == source.ID), skinChanged); + realmSubscription = realm?.RegisterForNotifications(r => r.All().Where(s => s.ID == source.ID), skinChanged); } protected override void Dispose(bool disposing) From da315f8a61b8e6d876dc47bfe709c17b8dc1241e Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Mon, 4 Apr 2022 22:44:35 +0200 Subject: [PATCH 275/285] Make the test hold the button instead of pressing it --- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index a0a1feff36..02a37627d7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -169,7 +169,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("click delete button", () => { InputManager.MoveMouseTo(dialogOverlay.ChildrenOfType().First()); - InputManager.Click(MouseButton.Left); + InputManager.PressButton(MouseButton.Left); }); AddUntilStep("wait for fetch", () => leaderboard.Scores != null); From b2c822a3b1b1498513829c9d469062a52a4a12ec Mon Sep 17 00:00:00 2001 From: CenTdemeern1 Date: Mon, 4 Apr 2022 23:02:07 +0200 Subject: [PATCH 276/285] Release mouse button --- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 02a37627d7..26e7b5bdda 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -174,6 +174,9 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("wait for fetch", () => leaderboard.Scores != null); AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID)); + + // "Clean up" + AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left)); } [Test] From 32c89f8643633fa7880292aa7d6828d7e6acaefa Mon Sep 17 00:00:00 2001 From: Ame Date: Tue, 5 Apr 2022 00:33:41 +0200 Subject: [PATCH 277/285] Handle repeated `OnPressed()` on `FooterButton` (without `FooterButtonRandom`) --- osu.Game/Screens/Select/FooterButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index 8d2ea47757..9cb178ca8b 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -174,7 +174,7 @@ namespace osu.Game.Screens.Select public virtual bool OnPressed(KeyBindingPressEvent e) { - if (e.Action == Hotkey) + if (e.Action == Hotkey && !e.Repeat) { TriggerClick(); return true; From 117d81d84f13c40fb7a782cfda8d1df6c5fa0c92 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 5 Apr 2022 03:04:53 +0300 Subject: [PATCH 278/285] Use perfect osu!(stable) strong scale value Co-authored-by: Dean Herbert --- osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs index 43a099b900..6e0f6a3109 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Objects /// /// Scale multiplier for a strong drawable taiko hit object. /// - public const float STRONG_SCALE = 1.525f; + public const float STRONG_SCALE = 1 / 0.65f; /// /// Default size of a strong drawable taiko hit object. From 174dc1641c3fcc03cf72d8f3d35105a07c732d3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Apr 2022 11:49:57 +0900 Subject: [PATCH 279/285] Fix multiple issues with timekeeping - Using realtime (`DateTimeOffset.Now`) meant that values would be changing in the same frame, causing misfirings or incorrect displays - No debounce on sample playback meant that scheduling edge cases could potentially cause samples to be played more than once. --- .../Match/MultiplayerReadyButton.cs | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index a9092bc25a..9b7e9a925e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } private MultiplayerCountdown countdown; - private DateTimeOffset countdownChangeTime; + private double countdownChangeTime; private ScheduledDelegate countdownUpdateDelegate; private void onRoomUpdated() => Scheduler.AddOnce(() => @@ -56,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (countdown != room?.Countdown) { countdown = room?.Countdown; - countdownChangeTime = DateTimeOffset.Now; + countdownChangeTime = Time.Current; } scheduleNextCountdownUpdate(); @@ -86,13 +86,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match int secondsRemaining = countdownTimeRemaining.Seconds; - if (secondsRemaining < 10) countdownTickSample?.Play(); - if (secondsRemaining <= 3) countdownTickFinalSample?.Play(); + playTickSound(secondsRemaining); - scheduleNextCountdownUpdate(); + if (secondsRemaining > 0) + scheduleNextCountdownUpdate(); } } + private double? lastTickSampleTime; + + private void playTickSound(int secondsRemaining) + { + // Simplified debounce. Ticks should only be played roughly once per second regardless of how often this function is called. + if (Time.Current - lastTickSampleTime < 500) + return; + + lastTickSampleTime = Time.Current; + + if (secondsRemaining < 10) countdownTickSample?.Play(); + if (secondsRemaining <= 3) countdownTickFinalSample?.Play(); + } + private void updateButtonText() { if (room == null) @@ -146,13 +160,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { get { - TimeSpan timeElapsed = DateTimeOffset.Now - countdownChangeTime; + double timeElapsed = Time.Current - countdownChangeTime; TimeSpan remaining; - if (timeElapsed > countdown.TimeRemaining) + if (timeElapsed > countdown.TimeRemaining.TotalMilliseconds) remaining = TimeSpan.Zero; else - remaining = countdown.TimeRemaining - timeElapsed; + remaining = countdown.TimeRemaining - TimeSpan.FromMilliseconds(timeElapsed); return remaining; } From 31bf0c4a9b78bb40f5175ea0cb0388243d95c0c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Apr 2022 13:16:06 +0900 Subject: [PATCH 280/285] Disable "final" sample in countdown for the time being --- .../OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 9b7e9a925e..6eed900963 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -104,7 +104,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match lastTickSampleTime = Time.Current; if (secondsRemaining < 10) countdownTickSample?.Play(); - if (secondsRemaining <= 3) countdownTickFinalSample?.Play(); + // disabled for now pending further work on sound effect + // if (secondsRemaining <= 3) countdownTickFinalSample?.Play(); } private void updateButtonText() From d0f83885cea57adb37334b3646f5eb2e75da784f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Apr 2022 13:20:34 +0900 Subject: [PATCH 281/285] Appease the CI --- .../OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 6eed900963..7e092bd353 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -30,13 +30,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private MultiplayerRoom room => multiplayerClient.Room; private Sample countdownTickSample; - private Sample countdownTickFinalSample; [BackgroundDependencyLoader] private void load(AudioManager audio) { countdownTickSample = audio.Samples.Get(@"Multiplayer/countdown-tick"); - countdownTickFinalSample = audio.Samples.Get(@"Multiplayer/countdown-tick-final"); + // disabled for now pending further work on sound effect + // countdownTickFinalSample = audio.Samples.Get(@"Multiplayer/countdown-tick-final"); } protected override void LoadComplete() From 5f415cbe53b1d406aa83abf402ceb7249305ea61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Apr 2022 15:48:18 +0900 Subject: [PATCH 282/285] Full potential null reference and add better commentary on countdown scheduling --- .../OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 7e092bd353..4cde7e71c3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -67,9 +67,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void scheduleNextCountdownUpdate() { + countdownUpdateDelegate?.Cancel(); + if (countdown != null) { - // If a countdown is active, schedule relevant components to update on the next whole second. + // The remaining time on a countdown may be at a fractional portion between two seconds. + // We want to align certain audio/visual cues to the point at which integer seconds change. + // To do so, we schedule to the next whole second. + // Note that scheduler invocation isn't guaranteed to be accurate, so this may still occur slightly late. double timeToNextSecond = countdownTimeRemaining.TotalMilliseconds % 1000; countdownUpdateDelegate = Scheduler.AddDelayed(onCountdownTick, timeToNextSecond); From 8e543204cdca8305cf224f338848876d31120982 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Apr 2022 15:49:47 +0900 Subject: [PATCH 283/285] Remove debounce logic (not required after switching to `Update` clock time) --- .../Multiplayer/Match/MultiplayerReadyButton.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 4cde7e71c3..4860078a79 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -98,16 +98,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } } - private double? lastTickSampleTime; - private void playTickSound(int secondsRemaining) { - // Simplified debounce. Ticks should only be played roughly once per second regardless of how often this function is called. - if (Time.Current - lastTickSampleTime < 500) - return; - - lastTickSampleTime = Time.Current; - if (secondsRemaining < 10) countdownTickSample?.Play(); // disabled for now pending further work on sound effect // if (secondsRemaining <= 3) countdownTickFinalSample?.Play(); From 3d8ae0465f1313981960f10a949aadbd21bd6651 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Apr 2022 15:51:04 +0900 Subject: [PATCH 284/285] Reword comment slightly --- .../OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 4860078a79..d275f309cb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -73,8 +73,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { // The remaining time on a countdown may be at a fractional portion between two seconds. // We want to align certain audio/visual cues to the point at which integer seconds change. - // To do so, we schedule to the next whole second. - // Note that scheduler invocation isn't guaranteed to be accurate, so this may still occur slightly late. + // To do so, we schedule to the next whole second. Note that scheduler invocation isn't + // guaranteed to be accurate, so this may still occur slightly late, but even in such a case + // the next invocation will be roughly correct. double timeToNextSecond = countdownTimeRemaining.TotalMilliseconds % 1000; countdownUpdateDelegate = Scheduler.AddDelayed(onCountdownTick, timeToNextSecond); From 2ec15a1ebe83c3e18bbde06f9f41c29db7780797 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 5 Apr 2022 16:47:15 +0900 Subject: [PATCH 285/285] Fix lookup through transformers --- osu.Game/Skinning/LegacySkinTransformer.cs | 2 +- osu.Game/Skinning/SkinnableSprite.cs | 23 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index 97084f34e0..9481fc7182 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -23,7 +23,7 @@ namespace osu.Game.Skinning /// The which is being transformed. /// [NotNull] - protected ISkin Skin { get; } + protected internal ISkin Skin { get; } protected LegacySkinTransformer([NotNull] ISkin skin) { diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index c5f110f908..4b4d7fe2c6 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -83,7 +84,7 @@ namespace osu.Game.Skinning // Round-about way of getting the user's skin to find available resources. // In the future we'll probably want to allow access to resources from the fallbacks, or potentially other skins // but that requires further thought. - var highestPrioritySkin = ((SkinnableSprite)SettingSourceObject).source.AllSources.First() as Skin; + var highestPrioritySkin = getHighestPriorityUserSkin(((SkinnableSprite)SettingSourceObject).source.AllSources) as Skin; string[] availableFiles = highestPrioritySkin?.SkinInfo.PerformRead(s => s.Files .Where(f => f.Filename.EndsWith(".png", StringComparison.Ordinal) @@ -92,6 +93,26 @@ namespace osu.Game.Skinning if (availableFiles?.Length > 0) Items = availableFiles; + + static ISkin getHighestPriorityUserSkin(IEnumerable skins) + { + foreach (var skin in skins) + { + if (skin is LegacySkinTransformer transformer && isUserSkin(transformer.Skin)) + return transformer.Skin; + + if (isUserSkin(skin)) + return skin; + } + + return null; + } + + // Temporarily used to exclude undesirable ISkin implementations + static bool isUserSkin(ISkin skin) + => skin.GetType() == typeof(DefaultSkin) + || skin.GetType() == typeof(DefaultLegacySkin) + || skin.GetType() == typeof(LegacySkin); } }