From f53ce5aedfd17d18eaa9a882e14707636a806a92 Mon Sep 17 00:00:00 2001 From: Henry Lin <henry.ys.lin@gmail.com> Date: Sun, 23 Jan 2022 11:11:12 +0800 Subject: [PATCH 001/113] 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<Slider>().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<Slider>().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 <henry.ys.lin@gmail.com> Date: Sun, 23 Jan 2022 11:25:22 +0800 Subject: [PATCH 002/113] 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 <henry.ys.lin@gmail.com> Date: Sun, 23 Jan 2022 13:00:54 +0800 Subject: [PATCH 003/113] 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<Slider>().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 <smoogipoo@smgi.me> Date: Wed, 16 Feb 2022 12:05:55 +0900 Subject: [PATCH 004/113] 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 /// </summary> new IReadOnlyList<T> HitObjects { get; } } + + public static class BeatmapExtensions + { + /// <summary> + /// Finds the maximum achievable combo by hitting all <see cref="HitObject"/>s in a beatmap. + /// </summary> + 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 <smoogipoo@smgi.me> Date: Thu, 17 Feb 2022 21:14:49 +0900 Subject: [PATCH 005/113] 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 /// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns> public IEnumerable<DifficultyAttributes> 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 <smoogipoo@smgi.me> Date: Tue, 22 Feb 2022 18:12:55 +0900 Subject: [PATCH 006/113] 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 /// <summary> /// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap. /// </summary> + /// <remarks> + /// This should only be used to compute difficulties for legacy mod combinations. + /// </remarks> /// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns> public IEnumerable<DifficultyAttributes> CalculateAll(CancellationToken cancellationToken = default) { From 37328f8d245fc9d50e3a14bfe57a3d9c73d0f1d1 Mon Sep 17 00:00:00 2001 From: Henry Lin <henry.ys.lin@gmail.com> Date: Wed, 9 Mar 2022 20:36:31 +0800 Subject: [PATCH 007/113] 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 <contact@ppy.sh>. 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 +{ + /// <summary> + /// Places hit objects according to information in <see cref="RandomObjects"/> while keeping objects inside the playfield. + /// </summary> + public class OsuHitObjectPositionModifier + { + /// <summary> + /// Number of previous hitobjects to be shifted together when an object is being moved. + /// </summary> + private const int preceding_hitobjects_to_shift = 10; + + private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2; + + private readonly List<OsuHitObject> hitObjects; + + private readonly List<RandomObjectInfo> randomObjects = new List<RandomObjectInfo>(); + + /// <summary> + /// Contains information specifying how each hit object should be placed. + /// <para>The default values correspond to how objects are originally placed in the beatmap.</para> + /// </summary> + public IReadOnlyList<IRandomObjectInfo> RandomObjects => randomObjects; + + public OsuHitObjectPositionModifier(List<OsuHitObject> 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; + } + } + + /// <summary> + /// Reposition the hit objects according to the information in <see cref="RandomObjects"/>. + /// </summary> + 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<OsuHitObject>(); + + 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; + } + } + + /// <summary> + /// Compute the randomised position of a hit object while attempting to keep it inside the playfield. + /// </summary> + /// <param name="current">The <see cref="RandomObjectInfo"/> representing the hit object to have the randomised position computed for.</param> + /// <param name="previous">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the current one.</param> + /// <param name="beforePrevious">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param> + 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; + } + + /// <summary> + /// Move the randomised position of a hit circle so that it fits inside the playfield. + /// </summary> + /// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns> + 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; + } + + /// <summary> + /// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already. + /// </summary> + /// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns> + 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; + } + + /// <summary> + /// Decreasingly shift a list of <see cref="OsuHitObject"/>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. + /// </summary> + /// <param name="hitObjects">The list of hit objects to be shifted.</param> + /// <param name="shift">The amount to be shifted.</param> + private void applyDecreasingShift(IList<OsuHitObject> 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); + } + } + + /// <summary> + /// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates) + /// such that the entire slider is inside the playfield. + /// </summary> + /// <remarks> + /// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height. + /// </remarks> + private RectangleF calculatePossibleMovementBounds(Slider slider) + { + var pathPositions = new List<Vector2>(); + 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); + } + + /// <summary> + /// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift. + /// </summary> + /// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param> + /// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param> + 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; + } + } + + /// <summary> + /// Clamp a position to playfield, keeping a specified distance from the edges. + /// </summary> + /// <param name="position">The position to be clamped.</param> + /// <param name="padding">The minimum distance allowed from playfield edges.</param> + /// <returns>The clamped position.</returns> + 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 + { + /// <summary> + /// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle. + /// </summary> + /// <remarks> + /// <see cref="RelativeAngle"/> of the first hit object in a beatmap represents the absolute angle from playfield center to the object. + /// </remarks> + /// <example> + /// If <see cref="RelativeAngle"/> is 0, the player's cursor doesn't need to change its direction of movement when passing + /// the previous object to reach this one. + /// </example> + float RelativeAngle { get; set; } + + /// <summary> + /// The jump distance from the previous hit object to this one. + /// </summary> + /// <remarks> + /// <see cref="DistanceFromPrevious"/> of the first hit object in a beatmap is relative to the playfield center. + /// </remarks> + float DistanceFromPrevious { get; set; } + + /// <summary> + /// The hit object associated with this <see cref="IRandomObjectInfo"/>. + /// </summary> + 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; - - /// <summary> - /// Number of previous hitobjects to be shifted together when another object is being moved. - /// </summary> - 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); - } - - /// <summary> - /// Randomise the position of each hit object and return a list of <see cref="RandomObjectInfo"/>s describing how each hit object should be placed. - /// </summary> - /// <param name="hitObjects">A list of <see cref="OsuHitObject"/>s to have their positions randomised.</param> - /// <returns>A list of <see cref="RandomObjectInfo"/>s describing how each hit object should be placed.</returns> - private List<RandomObjectInfo> randomiseObjects(IEnumerable<OsuHitObject> hitObjects) - { - Debug.Assert(rng != null, $"{nameof(ApplyToBeatmap)} was not called before randomising objects"); - - var randomObjects = new List<RandomObjectInfo>(); - 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; - } - - /// <summary> - /// Reposition the hit objects according to the information in <paramref name="randomObjects"/>. - /// </summary> - /// <param name="hitObjects">The hit objects to be repositioned.</param> - /// <param name="randomObjects">A list of <see cref="RandomObjectInfo"/> describing how each hit object should be placed.</param> - private void applyRandomisation(IReadOnlyList<OsuHitObject> hitObjects, IReadOnlyList<RandomObjectInfo> 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<OsuHitObject>(); - - 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; - } - } - - /// <summary> - /// Compute the randomised position of a hit object while attempting to keep it inside the playfield. - /// </summary> - /// <param name="current">The <see cref="RandomObjectInfo"/> representing the hit object to have the randomised position computed for.</param> - /// <param name="previous">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the current one.</param> - /// <param name="beforePrevious">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param> - 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; - } - - /// <summary> - /// Move the randomised position of a hit circle so that it fits inside the playfield. - /// </summary> - /// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns> - 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; - } - - /// <summary> - /// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already. - /// </summary> - /// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns> - 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; - } - - /// <summary> - /// Decreasingly shift a list of <see cref="OsuHitObject"/>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. - /// </summary> - /// <param name="hitObjects">The list of hit objects to be shifted.</param> - /// <param name="shift">The amount to be shifted.</param> - private void applyDecreasingShift(IList<OsuHitObject> 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); - } - } - - /// <summary> - /// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates) - /// such that the entire slider is inside the playfield. - /// </summary> - /// <remarks> - /// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height. - /// </remarks> - private RectangleF calculatePossibleMovementBounds(Slider slider) - { - var pathPositions = new List<Vector2>(); - 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); - } - - /// <summary> - /// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift. - /// </summary> - /// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param> - /// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param> - 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; - } - } - - /// <summary> - /// Clamp a position to playfield, keeping a specified distance from the edges. - /// </summary> - /// <param name="position">The position to be clamped.</param> - /// <param name="padding">The minimum distance allowed from playfield edges.</param> - /// <returns>The clamped position.</returns> - 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 - { - /// <summary> - /// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle. - /// </summary> - /// <remarks> - /// <see cref="RelativeAngle"/> of the first hit object in a beatmap represents the absolute angle from playfield center to the object. - /// </remarks> - /// <example> - /// If <see cref="RelativeAngle"/> is 0, the player's cursor doesn't need to change its direction of movement when passing - /// the previous object to reach this one. - /// </example> - public float RelativeAngle { get; set; } - - /// <summary> - /// The jump distance from the previous hit object to this one. - /// </summary> - /// <remarks> - /// <see cref="DistanceFromPrevious"/> of the first hit object in a beatmap is relative to the playfield center. - /// </remarks> - 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 <henry.ys.lin@gmail.com> Date: Wed, 9 Mar 2022 20:52:11 +0800 Subject: [PATCH 008/113] 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 { /// <summary> - /// Places hit objects according to information in <see cref="RandomObjects"/> while keeping objects inside the playfield. + /// Places hit objects according to information in <see cref="ObjectPositionInfos"/> while keeping objects inside the playfield. /// </summary> public class OsuHitObjectPositionModifier { @@ -28,21 +28,21 @@ namespace osu.Game.Rulesets.Osu.Mods private readonly List<OsuHitObject> hitObjects; - private readonly List<RandomObjectInfo> randomObjects = new List<RandomObjectInfo>(); + private readonly List<ObjectPositionInfo> objectPositionInfos = new List<ObjectPositionInfo>(); /// <summary> /// Contains information specifying how each hit object should be placed. /// <para>The default values correspond to how objects are originally placed in the beatmap.</para> /// </summary> - public IReadOnlyList<IRandomObjectInfo> RandomObjects => randomObjects; + public IReadOnlyList<IObjectPositionInfo> ObjectPositionInfos => objectPositionInfos; public OsuHitObjectPositionModifier(List<OsuHitObject> 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 } /// <summary> - /// Reposition the hit objects according to the information in <see cref="RandomObjects"/>. + /// Reposition the hit objects according to the information in <see cref="ObjectPositionInfos"/>. /// </summary> - 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 } /// <summary> - /// 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. /// </summary> - /// <param name="current">The <see cref="RandomObjectInfo"/> representing the hit object to have the randomised position computed for.</param> - /// <param name="previous">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the current one.</param> - /// <param name="beforePrevious">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param> - private void computeRandomisedPosition(RandomObjectInfo current, RandomObjectInfo? previous, RandomObjectInfo? beforePrevious) + /// <param name="current">The <see cref="ObjectPositionInfo"/> representing the hit object to have the modified position computed for.</param> + /// <param name="previous">The <see cref="ObjectPositionInfo"/> representing the hit object immediately preceding the current one.</param> + /// <param name="beforePrevious">The <see cref="ObjectPositionInfo"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param> + 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; } /// <summary> - /// 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. /// </summary> - /// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns> - private Vector2 clampHitCircleToPlayfield(HitCircle circle, RandomObjectInfo objectInfo) + /// <returns>The deviation from the original modified position in order to fit within the playfield.</returns> + 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; } /// <summary> /// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already. /// </summary> - /// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns> - private Vector2 clampSliderToPlayfield(Slider slider, RandomObjectInfo objectInfo) + /// <returns>The deviation from the original modified position in order to fit within the playfield.</returns> + 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; } /// <summary> @@ -293,7 +293,7 @@ namespace osu.Game.Rulesets.Osu.Mods ); } - public interface IRandomObjectInfo + public interface IObjectPositionInfo { /// <summary> /// 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; } /// <summary> - /// The hit object associated with this <see cref="IRandomObjectInfo"/>. + /// The hit object associated with this <see cref="IObjectPositionInfo"/>. /// </summary> 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 <henry.ys.lin@gmail.com> Date: Wed, 9 Mar 2022 21:04:35 +0800 Subject: [PATCH 009/113] 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 <henry.ys.lin@gmail.com> Date: Wed, 9 Mar 2022 21:52:15 +0800 Subject: [PATCH 010/113] 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 { /// <summary> /// Places hit objects according to information in <see cref="ObjectPositionInfos"/> while keeping objects inside the playfield. From ede838c4b3ebaa2fa2471de68ec11bbf7fbd21e5 Mon Sep 17 00:00:00 2001 From: Henry Lin <henry.ys.lin@gmail.com> Date: Thu, 10 Mar 2022 11:23:52 +0800 Subject: [PATCH 011/113] 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 <henry.ys.lin@gmail.com> Date: Thu, 10 Mar 2022 11:53:03 +0800 Subject: [PATCH 012/113] 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 { - /// <summary> - /// Places hit objects according to information in <see cref="ObjectPositionInfos"/> while keeping objects inside the playfield. - /// </summary> - public class OsuHitObjectPositionModifier + public static partial class OsuHitObjectGenerationUtils { /// <summary> /// 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<OsuHitObject> hitObjects; - - private readonly List<ObjectPositionInfo> objectPositionInfos = new List<ObjectPositionInfo>(); - /// <summary> - /// Contains information specifying how each hit object should be placed. - /// <para>The default values correspond to how objects are originally placed in the beatmap.</para> + /// Generate a list of <see cref="IObjectPositionInfo"/>s containing information for how the given list of + /// <see cref="OsuHitObject"/>s are positioned. /// </summary> - public IReadOnlyList<IObjectPositionInfo> ObjectPositionInfos => objectPositionInfos; - - public OsuHitObjectPositionModifier(List<OsuHitObject> hitObjects) - { - this.hitObjects = hitObjects; - populateObjectPositionInfos(); - } - - private void populateObjectPositionInfos() + /// <param name="hitObjects">A list of <see cref="OsuHitObject"/>s to process.</param> + /// <returns>A list of <see cref="IObjectPositionInfo"/>s describing how each hit object is positioned relative to the previous one.</returns> + public static List<IObjectPositionInfo> GeneratePositionInfos(IEnumerable<OsuHitObject> hitObjects) { + var positionInfos = new List<IObjectPositionInfo>(); 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; } /// <summary> - /// Reposition the hit objects according to the information in <see cref="ObjectPositionInfos"/>. + /// Reposition the hit objects according to the information in <paramref name="objectPositionInfos"/>. /// </summary> - public void ApplyModifications() + /// <param name="objectPositionInfos"></param> + /// <returns>The repositioned hit objects.</returns> + public static List<OsuHitObject> RepositionHitObjects(IEnumerable<IObjectPositionInfo> objectPositionInfos) { + List<ObjectPositionInfo> positionInfos = objectPositionInfos.Cast<ObjectPositionInfo>().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(); } /// <summary> @@ -123,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// <param name="current">The <see cref="ObjectPositionInfo"/> representing the hit object to have the modified position computed for.</param> /// <param name="previous">The <see cref="ObjectPositionInfo"/> representing the hit object immediately preceding the current one.</param> /// <param name="beforePrevious">The <see cref="ObjectPositionInfo"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param> - 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. /// </summary> /// <returns>The deviation from the original modified position in order to fit within the playfield.</returns> - 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 <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already. /// </summary> /// <returns>The deviation from the original modified position in order to fit within the playfield.</returns> - 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 /// </summary> /// <param name="hitObjects">The list of hit objects to be shifted.</param> /// <param name="shift">The amount to be shifted.</param> - private void applyDecreasingShift(IList<OsuHitObject> hitObjects, Vector2 shift) + private static void applyDecreasingShift(IList<OsuHitObject> hitObjects, Vector2 shift) { for (int i = 0; i < hitObjects.Count; i++) { @@ -219,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// <remarks> /// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height. /// </remarks> - private RectangleF calculatePossibleMovementBounds(Slider slider) + private static RectangleF calculatePossibleMovementBounds(Slider slider) { var pathPositions = new List<Vector2>(); slider.Path.GetPathToProgress(pathPositions, 0, 1); @@ -266,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// </summary> /// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param> /// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param> - 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 /// <param name="position">The position to be clamped.</param> /// <param name="padding">The minimum distance allowed from playfield edges.</param> /// <returns>The clamped position.</returns> - 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 <henry.ys.lin@gmail.com> Date: Thu, 10 Mar 2022 12:02:25 +0800 Subject: [PATCH 013/113] 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; /// <summary> - /// Generate a list of <see cref="IObjectPositionInfo"/>s containing information for how the given list of + /// Generate a list of <see cref="ObjectPositionInfo"/>s containing information for how the given list of /// <see cref="OsuHitObject"/>s are positioned. /// </summary> /// <param name="hitObjects">A list of <see cref="OsuHitObject"/>s to process.</param> - /// <returns>A list of <see cref="IObjectPositionInfo"/>s describing how each hit object is positioned relative to the previous one.</returns> - public static List<IObjectPositionInfo> GeneratePositionInfos(IEnumerable<OsuHitObject> hitObjects) + /// <returns>A list of <see cref="ObjectPositionInfo"/>s describing how each hit object is positioned relative to the previous one.</returns> + public static List<ObjectPositionInfo> GeneratePositionInfos(IEnumerable<OsuHitObject> hitObjects) { - var positionInfos = new List<IObjectPositionInfo>(); + var positionInfos = new List<ObjectPositionInfo>(); Vector2 previousPosition = playfield_centre; float previousAngle = 0; @@ -56,12 +56,12 @@ namespace osu.Game.Rulesets.Osu.Utils /// <summary> /// Reposition the hit objects according to the information in <paramref name="objectPositionInfos"/>. /// </summary> - /// <param name="objectPositionInfos"></param> + /// <param name="objectPositionInfos">Position information for each hit object.</param> /// <returns>The repositioned hit objects.</returns> - public static List<OsuHitObject> RepositionHitObjects(IEnumerable<IObjectPositionInfo> objectPositionInfos) + public static List<OsuHitObject> RepositionHitObjects(IEnumerable<ObjectPositionInfo> objectPositionInfos) { - List<ObjectPositionInfo> positionInfos = objectPositionInfos.Cast<ObjectPositionInfo>().ToList(); - ObjectPositionInfo? previous = null; + List<ObjectPositionInfoInternal> 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 /// <summary> /// Compute the modified position of a hit object while attempting to keep it inside the playfield. /// </summary> - /// <param name="current">The <see cref="ObjectPositionInfo"/> representing the hit object to have the modified position computed for.</param> - /// <param name="previous">The <see cref="ObjectPositionInfo"/> representing the hit object immediately preceding the current one.</param> - /// <param name="beforePrevious">The <see cref="ObjectPositionInfo"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param> - private static void computeModifiedPosition(ObjectPositionInfo current, ObjectPositionInfo? previous, ObjectPositionInfo? beforePrevious) + /// <param name="current">The <see cref="ObjectPositionInfoInternal"/> representing the hit object to have the modified position computed for.</param> + /// <param name="previous">The <see cref="ObjectPositionInfoInternal"/> representing the hit object immediately preceding the current one.</param> + /// <param name="beforePrevious">The <see cref="ObjectPositionInfoInternal"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param> + 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. /// </summary> /// <returns>The deviation from the original modified position in order to fit within the playfield.</returns> - 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 <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already. /// </summary> /// <returns>The deviation from the original modified position in order to fit within the playfield.</returns> - 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 { /// <summary> /// 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 <see cref="RelativeAngle"/> is 0, the player's cursor doesn't need to change its direction of movement when passing /// the previous object to reach this one. /// </example> - float RelativeAngle { get; set; } + public float RelativeAngle { get; set; } /// <summary> /// The jump distance from the previous hit object to this one. @@ -306,32 +306,33 @@ namespace osu.Game.Rulesets.Osu.Utils /// <remarks> /// <see cref="DistanceFromPrevious"/> of the first hit object in a beatmap is relative to the playfield center. /// </remarks> - float DistanceFromPrevious { get; set; } - - /// <summary> - /// The hit object associated with this <see cref="IObjectPositionInfo"/>. - /// </summary> - 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; } - + /// <summary> + /// The hit object associated with this <see cref="ObjectPositionInfo"/>. + /// </summary> 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 <henry.ys.lin@gmail.com> Date: Mon, 14 Mar 2022 20:18:30 +0800 Subject: [PATCH 014/113] 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 /// <returns>The repositioned hit objects.</returns> public static List<OsuHitObject> RepositionHitObjects(IEnumerable<ObjectPositionInfo> objectPositionInfos) { - List<ObjectPositionInfoInternal> positionInfos = objectPositionInfos.Select(o => new ObjectPositionInfoInternal(o)).ToList(); - ObjectPositionInfoInternal? previous = null; + List<WorkingObject> 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(); } /// <summary> /// Compute the modified position of a hit object while attempting to keep it inside the playfield. /// </summary> - /// <param name="current">The <see cref="ObjectPositionInfoInternal"/> representing the hit object to have the modified position computed for.</param> - /// <param name="previous">The <see cref="ObjectPositionInfoInternal"/> representing the hit object immediately preceding the current one.</param> - /// <param name="beforePrevious">The <see cref="ObjectPositionInfoInternal"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param> - private static void computeModifiedPosition(ObjectPositionInfoInternal current, ObjectPositionInfoInternal? previous, ObjectPositionInfoInternal? beforePrevious) + /// <param name="current">The <see cref="WorkingObject"/> representing the hit object to have the modified position computed for.</param> + /// <param name="previous">The <see cref="WorkingObject"/> representing the hit object immediately preceding the current one.</param> + /// <param name="beforePrevious">The <see cref="WorkingObject"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param> + 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. /// </summary> /// <returns>The deviation from the original modified position in order to fit within the playfield.</returns> - 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 <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already. /// </summary> /// <returns>The deviation from the original modified position in order to fit within the playfield.</returns> - 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 <henry.ys.lin@gmail.com> Date: Mon, 14 Mar 2022 20:23:35 +0800 Subject: [PATCH 015/113] 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 } /// <summary> - /// Move the modified position of a hit circle so that it fits inside the playfield. + /// Move the modified position of a <see cref="HitCircle"/> so that it fits inside the playfield. /// </summary> /// <returns>The deviation from the original modified position in order to fit within the playfield.</returns> - 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; } /// <summary> /// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already. /// </summary> /// <returns>The deviation from the original modified position in order to fit within the playfield.</returns> - 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; } /// <summary> From e44db4e726784b846c5d7b3b3213bfd59a8e16f6 Mon Sep 17 00:00:00 2001 From: Henry Lin <henry.ys.lin@gmail.com> Date: Fri, 25 Mar 2022 15:13:25 +0800 Subject: [PATCH 016/113] 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 de1fbda648a1ce13522b5f0a8faa602161df5e58 Mon Sep 17 00:00:00 2001 From: Salman Ahmed <frenzibyte@gmail.com> Date: Sun, 27 Mar 2022 20:54:56 +0300 Subject: [PATCH 017/113] 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 <frenzibyte@gmail.com> Date: Sun, 27 Mar 2022 20:57:00 +0300 Subject: [PATCH 018/113] 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 <frenzibyte@gmail.com> Date: Sun, 27 Mar 2022 21:38:55 +0300 Subject: [PATCH 019/113] 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 <dach.bartlomiej@gmail.com> --- .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 f049d7cb677e899f9984611ae19410b01a18c978 Mon Sep 17 00:00:00 2001 From: Jai Sharma <jai@jai.moe> Date: Tue, 29 Mar 2022 21:36:08 +0100 Subject: [PATCH 020/113] 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<Channel>` 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 <contact@ppy.sh>. 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<Channel> currentChannel = new Bindable<Channel>(); + + 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 <contact@ppy.sh>. 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<Channel> 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 <contact@ppy.sh>. 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 <jai@jai.moe> Date: Tue, 29 Mar 2022 22:50:24 +0100 Subject: [PATCH 021/113] 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 e7d2d94eeef6b11cc13660855b8267247ffc202c Mon Sep 17 00:00:00 2001 From: Jai Sharma <jai@jai.moe> Date: Wed, 30 Mar 2022 02:16:50 +0100 Subject: [PATCH 022/113] 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<Channel> 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 eec3fef7a66382337c2c2ee99c7f28ebb951407e Mon Sep 17 00:00:00 2001 From: Jai Sharma <jai@jai.moe> Date: Wed, 30 Mar 2022 20:25:23 +0100 Subject: [PATCH 023/113] 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 <jai@jai.moe> Date: Wed, 30 Mar 2022 21:01:28 +0100 Subject: [PATCH 024/113] 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 3ac0da2da305feac1ea30999e257027ffe37e51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com> Date: Sat, 26 Mar 2022 21:33:52 +0100 Subject: [PATCH 025/113] 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 <contact@ppy.sh>. 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 <contact@ppy.sh>. 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 40b6f3ff0a1f21da5f2b98b7ed6398906e8500c0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu <smoogipoo@smgi.me> Date: Thu, 31 Mar 2022 15:09:06 +0900 Subject: [PATCH 026/113] 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. /// </summary> /// <remarks> - /// This should only be used to compute difficulties for legacy mod combinations. + /// This can only be used to compute difficulties for legacy mod combinations. /// </remarks> /// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns> - public IEnumerable<DifficultyAttributes> CalculateAll(CancellationToken cancellationToken = default) + public IEnumerable<DifficultyAttributes> CalculateAllLegacyCombinations(CancellationToken cancellationToken = default) { var rulesetInstance = ruleset.CreateInstance(); From 0a34ce2509d1df942034704a9baa057010b1af47 Mon Sep 17 00:00:00 2001 From: Dean Herbert <pe@ppy.sh> Date: Thu, 31 Mar 2022 19:07:17 +0900 Subject: [PATCH 027/113] 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 <timo.herngreen@gmail.com> Date: Thu, 31 Mar 2022 16:26:53 +0200 Subject: [PATCH 028/113] 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<ToolbarClockDisplayMode> 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<ToolbarClockDisplayMode>(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 <timo.herngreen@gmail.com> Date: Thu, 31 Mar 2022 17:21:50 +0200 Subject: [PATCH 029/113] 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 <timo.herngreen@gmail.com> Date: Thu, 31 Mar 2022 20:11:07 +0200 Subject: [PATCH 030/113] 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<TimingControlPoint> { - private SettingsSlider<double> 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<double> - { - private const double sane_minimum = 60; - private const double sane_maximum = 240; - - private readonly BindableNumber<double> 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<double> 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 <timo.herngreen@gmail.com> Date: Thu, 31 Mar 2022 20:39:26 +0200 Subject: [PATCH 031/113] 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<ToolbarClockDisplayMode> 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<ToolbarClockDisplayMode>(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<ToolbarClockDisplayMode>(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 <timo.herngreen@gmail.com> Date: Thu, 31 Mar 2022 21:06:05 +0200 Subject: [PATCH 032/113] 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?= <dach.bartlomiej@gmail.com> Date: Thu, 31 Mar 2022 21:20:30 +0200 Subject: [PATCH 033/113] 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 <contact@ppy.sh>. 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?= <dach.bartlomiej@gmail.com> Date: Thu, 31 Mar 2022 22:09:03 +0200 Subject: [PATCH 034/113] 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?= <dach.bartlomiej@gmail.com> Date: Thu, 31 Mar 2022 22:19:08 +0200 Subject: [PATCH 035/113] 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() + /// <summary> + /// Creates a new <see cref="ShearedToggleButton"/> + /// </summary> + /// <param name="width"> + /// The width of the button. + /// <list type="bullet"> + /// <item>If a non-<see langword="null"/> value is provided, this button will have a fixed width equal to the provided value.</item> + /// <item>If a <see langword="null"/> value is provided (or the argument is omitted entirely), the button will autosize in width to fit the text.</item> + /// </list> + /// </param> + 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 <pe@ppy.sh> Date: Fri, 1 Apr 2022 10:53:14 +0900 Subject: [PATCH 036/113] 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 <pe@ppy.sh> Date: Fri, 1 Apr 2022 12:15:26 +0900 Subject: [PATCH 037/113] 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<Mod> 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<OsuHitObject> { - 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<Mod> 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<OsuHitObject> + internal class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject> { - 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<OsuHitObject>, 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(); /// <summary> /// 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 <pe@ppy.sh> Date: Fri, 1 Apr 2022 12:44:49 +0900 Subject: [PATCH 038/113] 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 <pe@ppy.sh> Date: Fri, 1 Apr 2022 13:03:48 +0900 Subject: [PATCH 039/113] 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 <pe@ppy.sh> Date: Fri, 1 Apr 2022 14:11:53 +0900 Subject: [PATCH 040/113] 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<SkinInfo>() + .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 <pe@ppy.sh> Date: Fri, 1 Apr 2022 14:22:26 +0900 Subject: [PATCH 041/113] 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 <pe@ppy.sh> Date: Fri, 1 Apr 2022 14:30:02 +0900 Subject: [PATCH 042/113] 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 /// <returns>The new instance.</returns> 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<Drawable>(); 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 <pe@ppy.sh> Date: Wed, 23 Mar 2022 15:10:26 +0900 Subject: [PATCH 043/113] 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 <pe@ppy.sh> Date: Wed, 23 Mar 2022 15:11:35 +0900 Subject: [PATCH 044/113] 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 <contact@ppy.sh>. 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 +{ + /// <summary> + /// Intended to be a test bed for skinning. May be removed at some point in the future. + /// </summary> + [UsedImplicitly] + public class SkinSprite : CompositeDrawable, ISkinnableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [SettingSource("Sprite name", "The filename of the sprite")] + public Bindable<string> SpriteName { get; } = new Bindable<string>(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<Skin> 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<string> 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 <see cref="RulesetSkinProvidingContainer"/> which adds extra legacy and toggle logic that may affect the lookup process. /// </remarks> [ExcludeFromDynamicCompile] - public class SkinManager : ISkinSource, IStorageResourceProvider, IModelImporter<SkinInfo> + public class SkinManager : ISkinSource, IStorageResourceProvider, IModelImporter<SkinInfo>, IModelManager<SkinInfo>, IModelFileManager<SkinInfo, RealmNamedFileUsage> { 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<SkinInfo> items, bool silent = false) + { + skinModelManager.Delete(items, silent); + } + + public void Undelete(List<SkinInfo> 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 /// <summary> /// A skinnable element which uses a stable sprite and can therefore share implementation logic. /// </summary> - 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 <pe@ppy.sh> Date: Thu, 24 Mar 2022 19:32:34 +0900 Subject: [PATCH 045/113] 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<object>.Source))?.SetValue(control, obj); controlType.GetProperty(nameof(SettingsItem<object>.LabelText))?.SetValue(control, attr.Label); controlType.GetProperty(nameof(SettingsItem<object>.TooltipText))?.SetValue(control, attr.Description); controlType.GetProperty(nameof(SettingsItem<object>.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; } + /// <summary> + /// The source component if this <see cref="SettingsItem{T}"/> was created via <see cref="SettingSourceAttribute"/>. + /// </summary> + public Drawable Source { get; internal set; } + private IHasCurrentValue<T> controlWithCurrent => Control as IHasCurrentValue<T>; protected override Container<Drawable> 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 <contact@ppy.sh>. 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<string> SpriteName { get; } = new Bindable<string>(string.Empty); [Resolved] private ISkinSource source { get; set; } + public IEnumerable<string> 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<string> + { + 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 <pe@ppy.sh> Date: Thu, 24 Mar 2022 19:09:17 +0900 Subject: [PATCH 046/113] 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<byte[]> createRealmBackedStore(BeatmapInfo beatmapInfo, IStorageResourceProvider? resources) { - if (resources == null) + if (resources == null || beatmapInfo.BeatmapSet == null) // should only ever be used in tests. return new ResourceStore<byte[]>(); - return new RealmBackedResourceStore(beatmapInfo.BeatmapSet, resources.Files, new[] { @"ogg" }); + return new RealmBackedResourceStore<BeatmapSetInfo>(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 <contact@ppy.sh>. 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<byte[]> + public class RealmBackedResourceStore<T> : ResourceStore<byte[]> + where T : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey { - private readonly Dictionary<string, string> fileToStoragePathMapping = new Dictionary<string, string>(); + private Lazy<Dictionary<string, string>> fileToStoragePathMapping; - public RealmBackedResourceStore(IHasRealmFiles source, IResourceStore<byte[]> underlyingStore, string[] extensions = null) + private readonly Live<T> liveSource; + + public RealmBackedResourceStore(Live<T> source, IResourceStore<byte[]> 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<string> 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<string> GetAvailableResources() => fileToStoragePathMapping.Keys; + return null; + } + + private void invalidateCache() => fileToStoragePathMapping = new Lazy<Dictionary<string, string>>(initialiseFileCache, LazyThreadSafetyMode.ExecutionAndPublication); + + private Dictionary<string, string> initialiseFileCache() => liveSource.PerformRead(source => + { + var dictionary = new Dictionary<string, string>(); + dictionary.Clear(); + foreach (var f in source.Files) + dictionary[f.Filename.ToLowerInvariant()] = f.File.GetStoragePath(); + + return dictionary; + }); + + public override IEnumerable<string> 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<SkinInfo> realmBackedStorage; + /// <summary> /// Construct a new skin. /// </summary> @@ -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>(SkinInfo, resources.Files, resources.RealmAccess); + + (storage as ResourceStore<byte[]>)?.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<byte[]> userFiles; + private IDisposable currentSkinSubscription; + /// <summary> /// The default skin. /// </summary> @@ -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<SkinInfo>().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<T>(IRealmCollection<T> 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 <pe@ppy.sh> Date: Thu, 24 Mar 2022 22:53:49 +0900 Subject: [PATCH 047/113] 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<Dictionary<string, string>> fileToStoragePathMapping; private readonly Live<T> liveSource; + private readonly IDisposable? realmSubscription; public RealmBackedResourceStore(Live<T> source, IResourceStore<byte[]> underlyingStore, RealmAccess? realm) : base(underlyingStore) @@ -29,9 +31,17 @@ namespace osu.Game.Skinning invalidateCache(); Debug.Assert(fileToStoragePathMapping != null); + + realmSubscription = realm?.RegisterForNotifications(r => r.All<T>().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<T> sender, ChangeSet changes, Exception error) => invalidateCache(); protected override IEnumerable<string> 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<SkinInfo> realmBackedStorage; /// <summary> @@ -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<byte[]> userFiles; - private IDisposable currentSkinSubscription; - /// <summary> /// The default skin. /// </summary> @@ -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<SkinInfo>().Where(s => s.ID == skin.NewValue.ID), realmSkinChanged); - }); }; CurrentSkin.Value = DefaultSkin; @@ -122,12 +112,6 @@ namespace osu.Game.Skinning }; } - private void realmSkinChanged<T>(IRealmCollection<T> 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 <pe@ppy.sh> Date: Fri, 25 Mar 2022 17:31:03 +0900 Subject: [PATCH 048/113] 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 <contact@ppy.sh>. 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 -{ - /// <summary> - /// Intended to be a test bed for skinning. May be removed at some point in the future. - /// </summary> - [UsedImplicitly] - public class SkinSprite : CompositeDrawable, ISkinnableDrawable - { - public bool UsesFixedAnchor { get; set; } - - [SettingSource("Sprite name", "The filename of the sprite", SettingControlType = typeof(SpriteSelectorControl))] - public Bindable<string> SpriteName { get; } = new Bindable<string>(string.Empty); - - [Resolved] - private ISkinSource source { get; set; } - - public IEnumerable<string> 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<string> - { - 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<SkinInfo> realmBackedStorage; + private readonly RealmBackedResourceStore<SkinInfo>? realmBackedStorage; /// <summary> /// 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 <contact@ppy.sh>. 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<string> SpriteName { get; } = new Bindable<string>(string.Empty); + + [Resolved] + private ISkinSource source { get; set; } + + public IEnumerable<string> 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<string> + { + 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 <pe@ppy.sh> Date: Fri, 25 Mar 2022 17:36:02 +0900 Subject: [PATCH 049/113] 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 <contact@ppy.sh>. 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<string> AvailableFiles => (source.AllSources.First() as Skin)?.SkinInfo.PerformRead(s => s.Files.Select(f => Path.GetFileNameWithoutExtension(f.Filename)).Distinct()); + public IEnumerable<string> 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 <pe@ppy.sh> Date: Fri, 1 Apr 2022 14:46:14 +0900 Subject: [PATCH 050/113] 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 <pe@ppy.sh> Date: Fri, 1 Apr 2022 15:14:53 +0900 Subject: [PATCH 051/113] 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<string> 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<string> { - 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 <pe@ppy.sh> Date: Fri, 1 Apr 2022 15:49:05 +0900 Subject: [PATCH 052/113] 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 <pe@ppy.sh> Date: Fri, 1 Apr 2022 16:00:51 +0900 Subject: [PATCH 053/113] 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<object>.Source))?.SetValue(control, obj); + controlType.GetProperty(nameof(SettingsItem<object>.SettingSourceObject))?.SetValue(control, obj); controlType.GetProperty(nameof(SettingsItem<object>.LabelText))?.SetValue(control, attr.Label); controlType.GetProperty(nameof(SettingsItem<object>.TooltipText))?.SetValue(control, attr.Description); controlType.GetProperty(nameof(SettingsItem<object>.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 /// <summary> /// The source component if this <see cref="SettingsItem{T}"/> was created via <see cref="SettingSourceAttribute"/>. /// </summary> - public Drawable Source { get; internal set; } + public object SettingSourceObject { get; internal set; } private IHasCurrentValue<T> controlWithCurrent => Control as IHasCurrentValue<T>; 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 <pe@ppy.sh> Date: Fri, 1 Apr 2022 16:16:49 +0900 Subject: [PATCH 054/113] 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 <pe@ppy.sh> Date: Fri, 1 Apr 2022 16:19:00 +0900 Subject: [PATCH 055/113] 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<SkinInfo>() - .Where(skin => !skin.DeletePending) - .AsEnumerable() - .Select(skin => skin.Name)); + string[] existingSkinNames = realm.Run(r => r.All<SkinInfo>() + .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 <frenzibyte@gmail.com> Date: Fri, 1 Apr 2022 10:40:16 +0300 Subject: [PATCH 056/113] Reword log retrieval steps on mobile platforms Co-authored-by: Dean Herbert <pe@ppy.sh> --- .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 <smoogipoo@smgi.me> Date: Fri, 1 Apr 2022 17:05:11 +0900 Subject: [PATCH 057/113] 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<DrawableRoomPlaylistItem>().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<DrawableRoomPlaylistItem>().Contains)); + } + /// <summary> /// Adds a step to create a new playlist item. /// </summary> - 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(); + }); /// <summary> /// Asserts the position of a given playlist item in the queue list. From 16d4544ff9b4ca72fcd0899c9cd56a1a92f91246 Mon Sep 17 00:00:00 2001 From: Dan Balasescu <smoogipoo@smgi.me> Date: Fri, 1 Apr 2022 17:06:37 +0900 Subject: [PATCH 058/113] 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<IBeatmapInfo> beatmap = default, Optional<ushort?> 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 <smoogipoo@smgi.me> Date: Fri, 1 Apr 2022 18:31:17 +0900 Subject: [PATCH 059/113] 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<bool> IsConnected => isConnected; private readonly Bindable<bool> isConnected = new Bindable<bool>(true); + /// <summary> + /// The local client's <see cref="Room"/>. This is not always equivalent to the server-side room. + /// </summary> public new Room? APIRoom => base.APIRoom; + public Action<MultiplayerRoom>? RoomSetupAction; public bool RoomJoined { get; private set; } @@ -46,6 +50,11 @@ namespace osu.Game.Tests.Visual.Multiplayer /// </summary> private readonly List<MultiplayerPlaylistItem> serverSidePlaylist = new List<MultiplayerPlaylistItem>(); + /// <summary> + /// Guaranteed up-to-date API room. + /// </summary> + 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<MultiplayerRoom> 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<MultiplayerPlaylistItem> 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 <pe@ppy.sh> Date: Fri, 1 Apr 2022 19:30:16 +0900 Subject: [PATCH 060/113] 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<SkinInfo>() .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 <me@nekodex.net> Date: Fri, 1 Apr 2022 21:33:57 +0900 Subject: [PATCH 061/113] 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 <timo.herngreen@gmail.com> Date: Sat, 2 Apr 2022 02:35:37 +0200 Subject: [PATCH 062/113] 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 <timo.herngreen@gmail.com> Date: Sat, 2 Apr 2022 02:43:44 +0200 Subject: [PATCH 063/113] 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 <timo.herngreen@gmail.com> Date: Sat, 2 Apr 2022 02:43:51 +0200 Subject: [PATCH 064/113] 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 <timo.herngreen@gmail.com> Date: Sat, 2 Apr 2022 02:59:07 +0200 Subject: [PATCH 065/113] 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 <timo.herngreen@gmail.com> Date: Sat, 2 Apr 2022 04:26:16 +0200 Subject: [PATCH 066/113] 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<ToolbarClockDisplayMode> 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<ToolbarClockDisplayMode>(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 <timo.herngreen@gmail.com> Date: Sat, 2 Apr 2022 04:31:43 +0200 Subject: [PATCH 067/113] 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 <timo.herngreen@gmail.com> Date: Sat, 2 Apr 2022 04:41:05 +0200 Subject: [PATCH 068/113] 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 <timo.herngreen@gmail.com> Date: Sat, 2 Apr 2022 04:45:21 +0200 Subject: [PATCH 069/113] 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 <timo.herngreen@gmail.com> Date: Sat, 2 Apr 2022 04:53:47 +0200 Subject: [PATCH 070/113] 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 <frenzibyte@gmail.com> Date: Sat, 2 Apr 2022 18:08:23 +0300 Subject: [PATCH 071/113] 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 <frenzibyte@gmail.com> Date: Sat, 2 Apr 2022 18:08:57 +0300 Subject: [PATCH 072/113] 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 <frenzibyte@gmail.com> Date: Sat, 2 Apr 2022 18:36:32 +0300 Subject: [PATCH 073/113] 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<BeatmapListingSearchControl>().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<BeatmapCardNormal>(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<BeatmapCardNormal>(100); - setCardSize(BeatmapCardSize.Extra); + setCardSize(BeatmapCardSize.Extra, viaConfig); assertAllCardsOfType<BeatmapCardExtra>(100); - setCardSize(BeatmapCardSize.Normal); + setCardSize(BeatmapCardSize.Normal, viaConfig); assertAllCardsOfType<BeatmapCardNormal>(100); AddStep("fetch for 0 beatmaps", () => fetchFor()); AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true); - setCardSize(BeatmapCardSize.Extra); + setCardSize(BeatmapCardSize.Extra, viaConfig); AddAssert("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true); } @@ -361,7 +374,13 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep("\"no maps found\" placeholder not shown", () => !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any(d => d.IsPresent)); } - private void setCardSize(BeatmapCardSize cardSize) => AddStep($"set card size to {cardSize}", () => overlay.ChildrenOfType<BeatmapListingCardSizeTabControl>().Single().Current.Value = cardSize); + private void setCardSize(BeatmapCardSize cardSize, bool viaConfig) => AddStep($"set card size to {cardSize}", () => + { + if (!viaConfig) + overlay.ChildrenOfType<BeatmapListingCardSizeTabControl>().Single().Current.Value = cardSize; + else + localConfig.SetValue(OsuSetting.BeatmapListingCardSize, cardSize); + }); private void assertAllCardsOfType<T>(int expectedCount) where T : BeatmapCard => @@ -370,5 +389,11 @@ namespace osu.Game.Tests.Visual.Online int loadedCorrectCount = this.ChildrenOfType<BeatmapCard>().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 <frenzibyte@gmail.com> Date: Sat, 2 Apr 2022 18:36:49 +0300 Subject: [PATCH 074/113] 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 /// <summary> /// The currently selected <see cref="BeatmapCardSize"/>. /// </summary> - public IBindable<BeatmapCardSize> CardSize { get; } = new Bindable<BeatmapCardSize>(); + public IBindable<BeatmapCardSize> CardSize => cardSize; + + private readonly Bindable<BeatmapCardSize> cardSize = new Bindable<BeatmapCardSize>(); 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 <jai@jai.moe> Date: Sat, 2 Apr 2022 17:14:27 +0100 Subject: [PATCH 075/113] 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 <jai@jai.moe> Date: Sat, 2 Apr 2022 17:15:19 +0100 Subject: [PATCH 076/113] 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<Channel> currentChannel = new Bindable<Channel>(); 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<string>? OnSearchTermsChanged; [Resolved] private Bindable<Channel> 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<string> 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 <frenzibyte@gmail.com> Date: Sat, 2 Apr 2022 19:24:16 +0300 Subject: [PATCH 077/113] 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 <dach.bartlomiej@gmail.com> --- .../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<BeatmapCardNormal>(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 <jai@jai.moe> Date: Sat, 2 Apr 2022 17:27:44 +0100 Subject: [PATCH 078/113] 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 <frenzibyte@gmail.com> Date: Sat, 2 Apr 2022 19:28:33 +0300 Subject: [PATCH 079/113] 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<BeatmapListingCardSizeTabControl>().Single().Current.Value = cardSize; - else + if (viaConfig) localConfig.SetValue(OsuSetting.BeatmapListingCardSize, cardSize); + else + overlay.ChildrenOfType<BeatmapListingCardSizeTabControl>().Single().Current.Value = cardSize; }); private void assertAllCardsOfType<T>(int expectedCount) From 0f9461689085cb790de5a3850d051a344d3f0fc2 Mon Sep 17 00:00:00 2001 From: Ame <ajiiisai@protonmail.com> Date: Sat, 2 Apr 2022 19:41:15 +0200 Subject: [PATCH 080/113] 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 <ajiiisai@protonmail.com> Date: Sat, 2 Apr 2022 20:45:00 +0200 Subject: [PATCH 081/113] 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<GlobalAction> 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 <frenzibyte@gmail.com> Date: Sat, 2 Apr 2022 23:47:20 +0300 Subject: [PATCH 082/113] 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 /// <summary> /// Default size of a drawable taiko hit object. /// </summary> - 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 /// <summary> /// Scale multiplier for a strong drawable taiko hit object. /// </summary> - public const float STRONG_SCALE = 1.4f; + public const float STRONG_SCALE = 1.525f; /// <summary> /// Default size of a strong drawable taiko hit object. From 94fa5e2ef20ab519484638d34be6bbb5238a256c Mon Sep 17 00:00:00 2001 From: Jai Sharma <jai@jai.moe> Date: Sat, 2 Apr 2022 21:58:54 +0100 Subject: [PATCH 083/113] Use `Action<string>` 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<string>? OnChatMessageCommitted; public event Action<string>? 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 <frenzibyte@gmail.com> Date: Sun, 3 Apr 2022 00:21:23 +0300 Subject: [PATCH 084/113] 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 /// </summary> 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 <frenzibyte@gmail.com> Date: Sun, 3 Apr 2022 01:21:48 +0300 Subject: [PATCH 085/113] 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?= <dach.bartlomiej@gmail.com> Date: Sun, 3 Apr 2022 14:23:45 +0200 Subject: [PATCH 086/113] 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?= <dach.bartlomiej@gmail.com> Date: Sun, 3 Apr 2022 14:27:37 +0200 Subject: [PATCH 087/113] 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?= <dach.bartlomiej@gmail.com> Date: Sun, 3 Apr 2022 15:24:00 +0200 Subject: [PATCH 088/113] 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<GlobalAction> 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 <smoogipoo@smgi.me> Date: Mon, 4 Apr 2022 13:46:41 +0900 Subject: [PATCH 089/113] 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 <pe@ppy.sh> Date: Mon, 4 Apr 2022 14:10:57 +0900 Subject: [PATCH 090/113] 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 @@ <Reference Include="Java.Interop" /> </ItemGroup> <ItemGroup> - <PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" /> + <PackageReference Include="ppy.osu.Game.Resources" Version="2022.404.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.325.0" /> </ItemGroup> <ItemGroup Label="Transitive Dependencies"> 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 @@ </PackageReference> <PackageReference Include="Realm" Version="10.10.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.325.0" /> - <PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" /> + <PackageReference Include="ppy.osu.Game.Resources" Version="2022.404.0" /> <PackageReference Include="Sentry" Version="3.14.1" /> <PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="NUnit" Version="3.13.2" /> 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 @@ </ItemGroup> <ItemGroup Label="Package References"> <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.325.0" /> - <PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" /> + <PackageReference Include="ppy.osu.Game.Resources" Version="2022.404.0" /> </ItemGroup> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) --> <PropertyGroup> From 0abebe4d23a17cfd772fe1b700ddefb2a2427a57 Mon Sep 17 00:00:00 2001 From: Dean Herbert <pe@ppy.sh> Date: Mon, 4 Apr 2022 14:36:03 +0900 Subject: [PATCH 091/113] 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 <pe@ppy.sh> Date: Mon, 4 Apr 2022 20:27:03 +0900 Subject: [PATCH 092/113] 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<T> liveSource; private readonly IDisposable? realmSubscription; - public RealmBackedResourceStore(Live<T> source, IResourceStore<byte[]> underlyingStore, RealmAccess? realm) + public RealmBackedResourceStore(Live<T> source, IResourceStore<byte[]> 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<T>().Where(s => s.ID == source.ID), skinChanged); + realmSubscription = realm.RegisterForNotifications(r => r.All<T>().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 <pe@ppy.sh> Date: Mon, 4 Apr 2022 20:27:46 +0900 Subject: [PATCH 093/113] 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 { /// <summary> - /// A skinnable element which uses a stable sprite and can therefore share implementation logic. + /// A skinnable element which uses a single texture backing. /// </summary> public class SkinnableSprite : SkinnableDrawable, ISkinnableDrawable { From dac5dfde8f8cee98be41887a5b4c94444efe3953 Mon Sep 17 00:00:00 2001 From: Dean Herbert <pe@ppy.sh> Date: Mon, 4 Apr 2022 20:28:43 +0900 Subject: [PATCH 094/113] 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<Dictionary<string, string>>(initialiseFileCache, LazyThreadSafetyMode.ExecutionAndPublication); + private void invalidateCache() => fileToStoragePathMapping = new Lazy<Dictionary<string, string>>(initialiseFileCache); private Dictionary<string, string> initialiseFileCache() => liveSource.PerformRead(source => { From de30a42558b7ab5d0e03cae2ed80d574cb694732 Mon Sep 17 00:00:00 2001 From: Dean Herbert <pe@ppy.sh> Date: Mon, 4 Apr 2022 20:30:14 +0900 Subject: [PATCH 095/113] 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<string> 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 <pe@ppy.sh> Date: Mon, 4 Apr 2022 20:35:48 +0900 Subject: [PATCH 096/113] 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 <pe@ppy.sh> Date: Mon, 4 Apr 2022 20:40:19 +0900 Subject: [PATCH 097/113] 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 <pe@ppy.sh> Date: Mon, 4 Apr 2022 20:44:05 +0900 Subject: [PATCH 098/113] 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 @@ </ItemGroup> <ItemGroup> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" /> - <PackageReference Include="ppy.osu.Framework.Android" Version="2022.325.0" /> + <PackageReference Include="ppy.osu.Framework.Android" Version="2022.404.0" /> </ItemGroup> <ItemGroup Label="Transitive Dependencies"> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> 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 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="Realm" Version="10.10.0" /> - <PackageReference Include="ppy.osu.Framework" Version="2022.325.0" /> + <PackageReference Include="ppy.osu.Framework" Version="2022.404.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" /> <PackageReference Include="Sentry" Version="3.14.1" /> <PackageReference Include="SharpCompress" Version="0.30.1" /> 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 @@ <Reference Include="System.Net.Http" /> </ItemGroup> <ItemGroup Label="Package References"> - <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.325.0" /> + <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.404.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" /> </ItemGroup> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) --> @@ -84,7 +84,7 @@ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> - <PackageReference Include="ppy.osu.Framework" Version="2022.325.0" /> + <PackageReference Include="ppy.osu.Framework" Version="2022.404.0" /> <PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> From 3708f2b744b46251c3d680b27ce7583109f3f2b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert <pe@ppy.sh> Date: Tue, 5 Apr 2022 01:05:04 +0900 Subject: [PATCH 099/113] 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 @@ <Reference Include="Java.Interop" /> </ItemGroup> <ItemGroup> - <PackageReference Include="ppy.osu.Game.Resources" Version="2022.404.0" /> + <PackageReference Include="ppy.osu.Game.Resources" Version="2022.405.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.404.0" /> </ItemGroup> <ItemGroup Label="Transitive Dependencies"> 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 @@ </PackageReference> <PackageReference Include="Realm" Version="10.10.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.404.0" /> - <PackageReference Include="ppy.osu.Game.Resources" Version="2022.404.0" /> + <PackageReference Include="ppy.osu.Game.Resources" Version="2022.405.0" /> <PackageReference Include="Sentry" Version="3.14.1" /> <PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="NUnit" Version="3.13.2" /> 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 @@ </ItemGroup> <ItemGroup Label="Package References"> <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.404.0" /> - <PackageReference Include="ppy.osu.Game.Resources" Version="2022.404.0" /> + <PackageReference Include="ppy.osu.Game.Resources" Version="2022.405.0" /> </ItemGroup> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) --> <PropertyGroup> From 9a07a95d39817ce408487253e410d845ee11c27d Mon Sep 17 00:00:00 2001 From: CenTdemeern1 <timo.herngreen@gmail.com> Date: Mon, 4 Apr 2022 19:22:53 +0200 Subject: [PATCH 100/113] 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?= <dach.bartlomiej@gmail.com> Date: Mon, 4 Apr 2022 21:59:09 +0200 Subject: [PATCH 101/113] 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<RulesetStore>(), () => 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<ScoreInfo>().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<ScoreInfo>().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<LeaderboardScore>().Any()); From f73062a0d6c141290c93f227db7dd63173175324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com> Date: Mon, 4 Apr 2022 22:22:55 +0200 Subject: [PATCH 102/113] 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<T> liveSource; private readonly IDisposable? realmSubscription; - public RealmBackedResourceStore(Live<T> source, IResourceStore<byte[]> underlyingStore, RealmAccess realm) + public RealmBackedResourceStore(Live<T> source, IResourceStore<byte[]> 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<T>().Where(s => s.ID == source.ID), skinChanged); + realmSubscription = realm?.RegisterForNotifications(r => r.All<T>().Where(s => s.ID == source.ID), skinChanged); } protected override void Dispose(bool disposing) From da315f8a61b8e6d876dc47bfe709c17b8dc1241e Mon Sep 17 00:00:00 2001 From: CenTdemeern1 <timo.herngreen@gmail.com> Date: Mon, 4 Apr 2022 22:44:35 +0200 Subject: [PATCH 103/113] 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<DialogButton>().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 <timo.herngreen@gmail.com> Date: Mon, 4 Apr 2022 23:02:07 +0200 Subject: [PATCH 104/113] 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 <ajiiisai@protonmail.com> Date: Tue, 5 Apr 2022 00:33:41 +0200 Subject: [PATCH 105/113] 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<GlobalAction> 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 <frenzibyte@gmail.com> Date: Tue, 5 Apr 2022 03:04:53 +0300 Subject: [PATCH 106/113] Use perfect osu!(stable) strong scale value Co-authored-by: Dean Herbert <pe@ppy.sh> --- 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 /// <summary> /// Scale multiplier for a strong drawable taiko hit object. /// </summary> - public const float STRONG_SCALE = 1.525f; + public const float STRONG_SCALE = 1 / 0.65f; /// <summary> /// Default size of a strong drawable taiko hit object. From 174dc1641c3fcc03cf72d8f3d35105a07c732d3e Mon Sep 17 00:00:00 2001 From: Dean Herbert <pe@ppy.sh> Date: Tue, 5 Apr 2022 11:49:57 +0900 Subject: [PATCH 107/113] 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 <pe@ppy.sh> Date: Tue, 5 Apr 2022 13:16:06 +0900 Subject: [PATCH 108/113] 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 <pe@ppy.sh> Date: Tue, 5 Apr 2022 13:20:34 +0900 Subject: [PATCH 109/113] 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 <pe@ppy.sh> Date: Tue, 5 Apr 2022 15:48:18 +0900 Subject: [PATCH 110/113] 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 <pe@ppy.sh> Date: Tue, 5 Apr 2022 15:49:47 +0900 Subject: [PATCH 111/113] 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 <pe@ppy.sh> Date: Tue, 5 Apr 2022 15:51:04 +0900 Subject: [PATCH 112/113] 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 <smoogipoo@smgi.me> Date: Tue, 5 Apr 2022 16:47:15 +0900 Subject: [PATCH 113/113] 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 <see cref="ISkin"/> which is being transformed. /// </summary> [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<ISkin> 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); } }