From f01deae42819902b0b1920c3caa5070243a6a9ac Mon Sep 17 00:00:00 2001 From: vun Date: Tue, 24 May 2022 17:38:52 +0800 Subject: [PATCH 001/532] Colour compression preprocessing implementation --- .../TaikoDifficultyHitObjectColour.cs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs new file mode 100644 index 0000000000..425f5a16a9 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs @@ -0,0 +1,78 @@ +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing +{ + /// + /// Stores colour compression information for a . + /// + public class TaikoDifficultyHitObjectColour + { + const int max_repetition_interval = 16; + + private TaikoDifficultyHitObjectColour previous; + + /// + /// True if the current colour is different from the previous colour. + /// + public bool Delta { get; private set; } + + /// + /// How many notes are Delta repeated + /// + public int DeltaRunLength { get; private set; } + + /// + /// How many notes between the current and previous identical . + /// Negative number means that there is no repetition in range. + /// + public int RepetitionInterval { get; private set; } + + /// + /// Get the instance for the given hitObject. This is implemented + /// as a static function instead of constructor to allow for reusing existing instances. + /// TODO: findRepetitionInterval needs to be called a final time after all hitObjects have been processed. + /// + public static TaikoDifficultyHitObjectColour GetInstanceFor( + TaikoDifficultyHitObject hitObject, TaikoDifficultyHitObject lastObject, TaikoDifficultyHitObjectColour previous) + { + bool delta = lastObject == null || hitObject.HitType != lastObject.HitType; + if (delta == previous.Delta) + { + previous.DeltaRunLength += 1; + return previous; + } + else + { + // Calculate RepetitionInterval for previous + previous.RepetitionInterval = findRepetitionInterval(previous); + + return new TaikoDifficultyHitObjectColour() + { + Delta = delta, + DeltaRunLength = 1, + RepetitionInterval = -1, + previous = previous + }; + } + } + + /// + /// Finds the closest previous that has the identical delta value + /// and run length to target, and returns the amount of notes between them. + /// + private static int findRepetitionInterval(TaikoDifficultyHitObjectColour target) { + if (target.previous == null || target.previous.previous == null) + return -1; + + int interval = target.previous.DeltaRunLength; + TaikoDifficultyHitObjectColour other = target.previous.previous; + while(other != null && interval < max_repetition_interval) { + if (other.Delta == target.Delta && other.DeltaRunLength == target.DeltaRunLength) + return interval; + else + interval += other.DeltaRunLength; + other = other.previous; + } + + return -1; + } + } +} \ No newline at end of file From 1972bdd6c7750b7a4f0170beeceebbe08c425906 Mon Sep 17 00:00:00 2001 From: vun Date: Thu, 26 May 2022 18:04:25 +0800 Subject: [PATCH 002/532] Working colour encoding --- .../Preprocessing/TaikoDifficultyHitObject.cs | 15 ++++--- .../TaikoDifficultyHitObjectColour.cs | 39 ++++++++++++------- .../Difficulty/TaikoDifficultyCalculator.cs | 11 +++++- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index ae33c184d0..97b79f5eea 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public readonly TaikoDifficultyHitObjectRhythm Rhythm; + public readonly TaikoDifficultyHitObjectColour Colour; + /// /// The hit type of this hit object. /// @@ -29,29 +31,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public readonly int ObjectIndex; - /// - /// Whether the object should carry a penalty due to being hittable using special techniques - /// making it easier to do so. - /// - public bool StaminaCheese; - /// /// Creates a new difficulty hit object. /// /// The gameplay associated with this difficulty object. /// The gameplay preceding . /// The gameplay preceding . + /// The for . /// The rate of the gameplay clock. Modified by speed-changing mods. /// The index of the object in the beatmap. - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex) + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, TaikoDifficultyHitObject lastDifficulty, double clockRate, int objectIndex) : base(hitObject, lastObject, clockRate) { var currentHit = hitObject as Hit; Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate); HitType = currentHit?.Type; - ObjectIndex = objectIndex; + + // Need to be done after HitType is set. + Colour = TaikoDifficultyHitObjectColour.GetInstanceFor(this, lastDifficulty); } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs index 425f5a16a9..2fada1c543 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs @@ -31,10 +31,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// TODO: findRepetitionInterval needs to be called a final time after all hitObjects have been processed. /// public static TaikoDifficultyHitObjectColour GetInstanceFor( - TaikoDifficultyHitObject hitObject, TaikoDifficultyHitObject lastObject, TaikoDifficultyHitObjectColour previous) + TaikoDifficultyHitObject hitObject, TaikoDifficultyHitObject lastObject) { + TaikoDifficultyHitObjectColour previous = lastObject?.Colour; bool delta = lastObject == null || hitObject.HitType != lastObject.HitType; - if (delta == previous.Delta) + if (previous != null && delta == previous.Delta) { previous.DeltaRunLength += 1; return previous; @@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing else { // Calculate RepetitionInterval for previous - previous.RepetitionInterval = findRepetitionInterval(previous); + previous?.FindRepetitionInterval(); return new TaikoDifficultyHitObjectColour() { @@ -56,23 +57,35 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// /// Finds the closest previous that has the identical delta value - /// and run length to target, and returns the amount of notes between them. + /// and run length with the current instance, and returns the amount of notes between them. /// - private static int findRepetitionInterval(TaikoDifficultyHitObjectColour target) { - if (target.previous == null || target.previous.previous == null) - return -1; + public void FindRepetitionInterval() + { + if (this.previous == null || this.previous.previous == null) + { + this.RepetitionInterval = -1; + return; + } - int interval = target.previous.DeltaRunLength; - TaikoDifficultyHitObjectColour other = target.previous.previous; - while(other != null && interval < max_repetition_interval) { - if (other.Delta == target.Delta && other.DeltaRunLength == target.DeltaRunLength) - return interval; + + int interval = this.previous.DeltaRunLength; + TaikoDifficultyHitObjectColour other = this.previous.previous; + while (other != null && interval < max_repetition_interval) + { + if (other.Delta == this.Delta && other.DeltaRunLength == this.DeltaRunLength) + { + this.RepetitionInterval = interval; + return; + } else + { interval += other.DeltaRunLength; + } + other = other.previous; } - return -1; + this.RepetitionInterval = -1; } } } \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 1aa31c6fe4..6697ad0509 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -52,11 +52,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { taikoDifficultyHitObjects.Add( new TaikoDifficultyHitObject( - beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i + beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], taikoDifficultyHitObjects.DefaultIfEmpty(null).LastOrDefault(), clockRate, i ) ); } + // Find repetition interval for the final TaikoDifficultyHitObjectColour + // TODO: Might be a good idea to refactor this + taikoDifficultyHitObjects.Last().Colour.FindRepetitionInterval(); + + taikoDifficultyHitObjects.ForEach((item) => + { + Console.WriteLine($"{item.StartTime}, {item.Colour.GetHashCode()}, {item.Colour.Delta}, {item.Colour.DeltaRunLength}, {item.Colour.RepetitionInterval}"); + }); + return taikoDifficultyHitObjects; } From 86ffa810a9628ace6daaee54244e41dde0297563 Mon Sep 17 00:00:00 2001 From: vun Date: Tue, 31 May 2022 23:17:39 +0800 Subject: [PATCH 003/532] Implement stamina evaluator (untested yet) --- .../Difficulty/Evaluators/StaminaEvaluator.cs | 64 +++++++++++++++++++ .../Difficulty/Skills/SingleKeyStamina.cs | 42 ------------ .../Difficulty/Skills/Stamina.cs | 48 +------------- 3 files changed, 66 insertions(+), 88 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/SingleKeyStamina.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs new file mode 100644 index 0000000000..a2f6b860f6 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators +{ + public class StaminaEvaluator + { + /// + /// Applies a speed bonus dependent on the time since the last hit performed using this key. + /// + /// The duration between the current and previous note hit using the same key. + private static double speedBonus(double notePairDuration) + { + return 175 / (notePairDuration + 100); + } + + /// + /// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the + /// maximum possible interval between two hits using the same key, by alternating 2 keys for each colour. + /// + public static double EvaluateDifficultyOf(DifficultyHitObject current) + { + if (!(current.BaseObject is Hit)) + { + return 0.0; + } + + // Find the previous hit object hit by the current key, which is two notes of the same colour prior. + // TODO: This could result in potential performance issue where it has to check the colour of a large amount + // of objects due to previous objects being mono of the other colour. A potential fix for this would be + // to store two separate lists of previous objects, one for each colour. + TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; + TaikoDifficultyHitObject previous = taikoCurrent; + int monoNoteInterval = 2; // The amount of same-colour notes to go back + double currentKeyInterval = 0; // Interval of the current key being pressed + do + { + previous = (TaikoDifficultyHitObject)previous.Previous(1); + if (previous.BaseObject is Hit && previous.HitType == taikoCurrent.HitType) + { + --monoNoteInterval; + } + currentKeyInterval += previous.DeltaTime; + + } while (previous != null && monoNoteInterval > 0); + + // This note is the first press of the current key + if (monoNoteInterval > 0) + { + return 0; + } + + double objectStrain = 0.5; + objectStrain += speedBonus(currentKeyInterval); + return objectStrain; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SingleKeyStamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SingleKeyStamina.cs deleted file mode 100644 index cabfd231d8..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SingleKeyStamina.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Difficulty.Skills; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Skills -{ - /// - /// Stamina of a single key, calculated based on repetition speed. - /// - public class SingleKeyStamina - { - private double? previousHitTime; - - /// - /// Similar to - /// - public double StrainValueOf(DifficultyHitObject current) - { - if (previousHitTime == null) - { - previousHitTime = current.StartTime; - return 0; - } - - double objectStrain = 0.5; - objectStrain += speedBonus(current.StartTime - previousHitTime.Value); - previousHitTime = current.StartTime; - return objectStrain; - } - - /// - /// Applies a speed bonus dependent on the time since the last hit performed using this key. - /// - /// The duration between the current and previous note hit using the same key. - private double speedBonus(double notePairDuration) - { - return 175 / (notePairDuration + 100); - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 61bcbfa59d..71713bcd56 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -4,6 +4,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -20,28 +21,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; - private readonly SingleKeyStamina[] centreKeyStamina = - { - new SingleKeyStamina(), - new SingleKeyStamina() - }; - - private readonly SingleKeyStamina[] rimKeyStamina = - { - new SingleKeyStamina(), - new SingleKeyStamina() - }; - - /// - /// Current index into for a centre hit. - /// - private int centreKeyIndex; - - /// - /// Current index into for a rim hit. - /// - private int rimKeyIndex; - /// /// Creates a skill. /// @@ -51,32 +30,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { } - /// - /// Get the next to use for the given . - /// - /// The current . - private SingleKeyStamina getNextSingleKeyStamina(TaikoDifficultyHitObject current) - { - // Alternate key for the same color. - if (current.HitType == HitType.Centre) - { - centreKeyIndex = (centreKeyIndex + 1) % 2; - return centreKeyStamina[centreKeyIndex]; - } - - rimKeyIndex = (rimKeyIndex + 1) % 2; - return rimKeyStamina[rimKeyIndex]; - } - protected override double StrainValueOf(DifficultyHitObject current) { - if (!(current.BaseObject is Hit)) - { - return 0.0; - } - - TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; - return getNextSingleKeyStamina(hitObject).StrainValueOf(hitObject); + return StaminaEvaluator.EvaluateDifficultyOf(current); } } } From 8bbe70bff0ad69fc2cde3fe24281617dc18d3840 Mon Sep 17 00:00:00 2001 From: vun Date: Wed, 1 Jun 2022 04:33:37 +0800 Subject: [PATCH 004/532] Fix NullPointerReference --- .../Difficulty/Evaluators/StaminaEvaluator.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index a2f6b860f6..59d3e61a27 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -42,19 +40,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators do { previous = (TaikoDifficultyHitObject)previous.Previous(1); + if (previous == null) return 0; // No previous (The note is the first press of the current key) if (previous.BaseObject is Hit && previous.HitType == taikoCurrent.HitType) { --monoNoteInterval; } currentKeyInterval += previous.DeltaTime; - } while (previous != null && monoNoteInterval > 0); - - // This note is the first press of the current key - if (monoNoteInterval > 0) - { - return 0; - } + } while (monoNoteInterval > 0); double objectStrain = 0.5; objectStrain += speedBonus(currentKeyInterval); From 0a21f7c30dd15dbde01690de9a240b6e11e6b8a5 Mon Sep 17 00:00:00 2001 From: vun Date: Wed, 1 Jun 2022 05:20:08 +0800 Subject: [PATCH 005/532] Implement mono history in TaikoDifficultyHitObject --- .../Difficulty/Evaluators/StaminaEvaluator.cs | 23 +++++------------ .../Preprocessing/TaikoDifficultyHitObject.cs | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index 59d3e61a27..6f141f50ae 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -30,27 +30,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators } // Find the previous hit object hit by the current key, which is two notes of the same colour prior. - // TODO: This could result in potential performance issue where it has to check the colour of a large amount - // of objects due to previous objects being mono of the other colour. A potential fix for this would be - // to store two separate lists of previous objects, one for each colour. TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; - TaikoDifficultyHitObject previous = taikoCurrent; - int monoNoteInterval = 2; // The amount of same-colour notes to go back - double currentKeyInterval = 0; // Interval of the current key being pressed - do + TaikoDifficultyHitObject keyPrevious = taikoCurrent.PreviousMono(1); + if (keyPrevious == null) { - previous = (TaikoDifficultyHitObject)previous.Previous(1); - if (previous == null) return 0; // No previous (The note is the first press of the current key) - if (previous.BaseObject is Hit && previous.HitType == taikoCurrent.HitType) - { - --monoNoteInterval; - } - currentKeyInterval += previous.DeltaTime; - - } while (monoNoteInterval > 0); + // There is no previous hit object hit by the current key + return 0.0; + } double objectStrain = 0.5; - objectStrain += speedBonus(currentKeyInterval); + objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime); return objectStrain; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 450eb63636..699d08e7bc 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -15,6 +15,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public class TaikoDifficultyHitObject : DifficultyHitObject { + // TODO: Review this - these was originally handled in TaikodifficultyCalculator.CreateDifficultyHitObjects, but + // it might be a good idea to encapsulate as much detail within the class as possible. + private static List centreHitObjects = new List(); + private static List rimHitObjects = new List(); + + private readonly IReadOnlyList monoDifficultyHitObjects; + public readonly int MonoPosition; + /// /// The rhythm required to hit this hit object. /// @@ -47,6 +55,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate); HitType = currentHit?.Type; + + if (HitType == Objects.HitType.Centre) + { + MonoPosition = centreHitObjects.Count(); + centreHitObjects.Add(this); + monoDifficultyHitObjects = centreHitObjects; + } + else if (HitType == Objects.HitType.Rim) + { + MonoPosition = rimHitObjects.Count(); + rimHitObjects.Add(this); + monoDifficultyHitObjects = rimHitObjects; + } } /// @@ -85,5 +106,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); } + + public TaikoDifficultyHitObject PreviousMono(int backwardsIndex) => monoDifficultyHitObjects.ElementAtOrDefault(MonoPosition - (backwardsIndex + 1)); + + public TaikoDifficultyHitObject NextMono(int forwardsIndex) => monoDifficultyHitObjects.ElementAtOrDefault(MonoPosition + (forwardsIndex + 1)); } } From 56a4034c2260f8f6d4c2e0e37b4119e912fed021 Mon Sep 17 00:00:00 2001 From: vun Date: Thu, 2 Jun 2022 18:48:36 +0800 Subject: [PATCH 006/532] Change RepetitionInterval to have max_repetition_interval + 1 when no repetition is found. --- .../Preprocessing/TaikoDifficultyHitObjectColour.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs index 2fada1c543..a55fdc6e9f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs @@ -1,3 +1,5 @@ +using System; + namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { /// @@ -22,6 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// /// How many notes between the current and previous identical . /// Negative number means that there is no repetition in range. + /// If no repetition is found this will have a value of + 1. /// public int RepetitionInterval { get; private set; } @@ -49,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { Delta = delta, DeltaRunLength = 1, - RepetitionInterval = -1, + RepetitionInterval = max_repetition_interval + 1, previous = previous }; } @@ -63,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { if (this.previous == null || this.previous.previous == null) { - this.RepetitionInterval = -1; + this.RepetitionInterval = max_repetition_interval + 1; return; } @@ -74,7 +77,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { if (other.Delta == this.Delta && other.DeltaRunLength == this.DeltaRunLength) { - this.RepetitionInterval = interval; + this.RepetitionInterval = Math.Max(interval, max_repetition_interval); return; } else @@ -85,7 +88,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing other = other.previous; } - this.RepetitionInterval = -1; + this.RepetitionInterval = max_repetition_interval + 1; } } } \ No newline at end of file From 3dd0c4aec87c66c0527f9444a0a9bc9a80dbbfff Mon Sep 17 00:00:00 2001 From: vun Date: Mon, 6 Jun 2022 12:42:49 +0800 Subject: [PATCH 007/532] [WIP] Colour rework --- .../Difficulty/Evaluators/ColourEvaluator.cs | 33 +++++ .../Preprocessing/TaikoDifficultyHitObject.cs | 18 ++- .../TaikoDifficultyHitObjectColour.cs | 4 +- .../Difficulty/Skills/Colour.cs | 113 +----------------- .../Difficulty/Skills/SingleKeyStamina.cs | 20 +++- .../Difficulty/Skills/Stamina.cs | 9 +- .../Difficulty/TaikoDifficultyCalculator.cs | 26 ++-- 7 files changed, 88 insertions(+), 135 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs new file mode 100644 index 0000000000..159f9a4508 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -0,0 +1,33 @@ +using System; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators +{ + public class ColourEvaluator + { + private static double sigmoid(double val, double center, double width) + { + return Math.Tanh(Math.E * -(val - center) / width); + } + + public static double EvaluateDifficultyOf(DifficultyHitObject current) + { + TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; + TaikoDifficultyHitObjectColour colour = taikoCurrent.Colour; + if (colour == null) return 0; + double objectStrain = 1; + if (colour.Delta) + { + objectStrain /= Math.Pow(colour.DeltaRunLength, 0.25); + } + else + { + objectStrain *= sigmoid(colour.DeltaRunLength, 3, 3) * 0.3 + 0.3; + } + objectStrain *= -sigmoid(colour.RepetitionInterval, 8, 8); + // Console.WriteLine($"{current.StartTime},{colour.GetHashCode()},{colour.Delta},{colour.DeltaRunLength},{colour.RepetitionInterval},{objectStrain}"); + return objectStrain; + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index b34e87bc83..8d2eadafe1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -20,6 +20,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public readonly TaikoDifficultyHitObjectRhythm Rhythm; + /// + /// Colour data for this hit object. This is used by colour evaluator to calculate colour, but can be used + /// differently by other skills in the future. + /// public readonly TaikoDifficultyHitObjectColour Colour; /// @@ -45,7 +49,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing HitType = currentHit?.Type; // Need to be done after HitType is set. - Colour = TaikoDifficultyHitObjectColour.GetInstanceFor(this, (TaikoDifficultyHitObject) objects.LastOrDefault()); + if (HitType != null) + { + // Get previous hit object, while skipping one that does not have defined colour (sliders and spinners). + // Without skipping through these, sliders and spinners would have contributed to a colour change for the next note. + TaikoDifficultyHitObject previousHitObject = (TaikoDifficultyHitObject)objects.LastOrDefault(); + while (previousHitObject != null && previousHitObject.Colour == null) + { + previousHitObject = (TaikoDifficultyHitObject)previousHitObject.Previous(0); + } + + Colour = TaikoDifficultyHitObjectColour.GetInstanceFor(this); + } + } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs index a55fdc6e9f..ce65fd0552 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs @@ -33,9 +33,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// as a static function instead of constructor to allow for reusing existing instances. /// TODO: findRepetitionInterval needs to be called a final time after all hitObjects have been processed. /// - public static TaikoDifficultyHitObjectColour GetInstanceFor( - TaikoDifficultyHitObject hitObject, TaikoDifficultyHitObject lastObject) + public static TaikoDifficultyHitObjectColour GetInstanceFor(TaikoDifficultyHitObject hitObject) { + TaikoDifficultyHitObject lastObject = (TaikoDifficultyHitObject) hitObject.Previous(0); TaikoDifficultyHitObjectColour previous = lastObject?.Colour; bool delta = lastObject == null || hitObject.HitType != lastObject.HitType; if (previous != null && delta == previous.Delta) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 0c17ca66b9..9a8b350e22 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -6,7 +6,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills @@ -19,27 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; - /// - /// Maximum number of entries to keep in . - /// - private const int mono_history_max_length = 5; - - /// - /// Queue with the lengths of the last most recent mono (single-colour) patterns, - /// with the most recent value at the end of the queue. - /// - private readonly LimitedCapacityQueue monoHistory = new LimitedCapacityQueue(mono_history_max_length); - - /// - /// The of the last object hit before the one being considered. - /// - private HitType? previousHitType; - - /// - /// Length of the current mono pattern. - /// - private int currentMonoLength; - public Colour(Mod[] mods) : base(mods) { @@ -47,95 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double StrainValueOf(DifficultyHitObject current) { - // changing from/to a drum roll or a swell does not constitute a colour change. - // hits spaced more than a second apart are also exempt from colour strain. - if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)) - { - monoHistory.Clear(); - - var currentHit = current.BaseObject as Hit; - currentMonoLength = currentHit != null ? 1 : 0; - previousHitType = currentHit?.Type; - - return 0.0; - } - - var taikoCurrent = (TaikoDifficultyHitObject)current; - - double objectStrain = 0.0; - - if (previousHitType != null && taikoCurrent.HitType != previousHitType) - { - // The colour has changed. - objectStrain = 1.0; - - if (monoHistory.Count < 2) - { - // There needs to be at least two streaks to determine a strain. - objectStrain = 0.0; - } - else if ((monoHistory[^1] + currentMonoLength) % 2 == 0) - { - // The last streak in the history is guaranteed to be a different type to the current streak. - // If the total number of notes in the two streaks is even, nullify this object's strain. - objectStrain = 0.0; - } - - objectStrain *= repetitionPenalties(); - currentMonoLength = 1; - } - else - { - currentMonoLength += 1; - } - - previousHitType = taikoCurrent.HitType; - return objectStrain; + return ColourEvaluator.EvaluateDifficultyOf(current); } - - /// - /// The penalty to apply due to the length of repetition in colour streaks. - /// - private double repetitionPenalties() - { - const int most_recent_patterns_to_compare = 2; - double penalty = 1.0; - - monoHistory.Enqueue(currentMonoLength); - - for (int start = monoHistory.Count - most_recent_patterns_to_compare - 1; start >= 0; start--) - { - if (!isSamePattern(start, most_recent_patterns_to_compare)) - continue; - - int notesSince = 0; - for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i]; - penalty *= repetitionPenalty(notesSince); - break; - } - - return penalty; - } - - /// - /// Determines whether the last patterns have repeated in the history - /// of single-colour note sequences, starting from . - /// - private bool isSamePattern(int start, int mostRecentPatternsToCompare) - { - for (int i = 0; i < mostRecentPatternsToCompare; i++) - { - if (monoHistory[start + i] != monoHistory[monoHistory.Count - mostRecentPatternsToCompare + i]) - return false; - } - - return true; - } - - /// - /// Calculates the strain penalty for a colour pattern repetition. - /// - /// The number of notes since the last repetition of the pattern. - private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SingleKeyStamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SingleKeyStamina.cs index cabfd231d8..4b8a8033a4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SingleKeyStamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SingleKeyStamina.cs @@ -1,8 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { @@ -11,6 +13,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// public class SingleKeyStamina { + private const double StrainDecayBase = 0.4; + + private double CurrentStrain = 0; + private double? previousHitTime; /// @@ -24,19 +30,23 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return 0; } - double objectStrain = 0.5; - objectStrain += speedBonus(current.StartTime - previousHitTime.Value); + // CurrentStrain += strainDecay(current.StartTime - current.Previous(0).StartTime); + // CurrentStrain += 0.5 + 0.5 * strainDecay(current.StartTime - current.Previous(0).StartTime); + CurrentStrain += 1; + CurrentStrain *= ColourEvaluator.EvaluateDifficultyOf(current) * 0.1 + 0.9; + CurrentStrain *= strainDecay(current.StartTime - previousHitTime.Value); previousHitTime = current.StartTime; - return objectStrain; + return CurrentStrain; } /// /// Applies a speed bonus dependent on the time since the last hit performed using this key. /// /// The duration between the current and previous note hit using the same key. - private double speedBonus(double notePairDuration) + private double strainDecay(double notePairDuration) { - return 175 / (notePairDuration + 100); + return Math.Pow(StrainDecayBase, notePairDuration / 1000); + // return 175 / (notePairDuration + 100); } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 61bcbfa59d..7196b68df2 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -5,6 +5,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills @@ -17,8 +18,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// public class Stamina : StrainDecaySkill { - protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.4; + protected override double SkillMultiplier => 3.6; + protected override double StrainDecayBase => 0; private readonly SingleKeyStamina[] centreKeyStamina = { @@ -76,7 +77,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills } TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; - return getNextSingleKeyStamina(hitObject).StrainValueOf(hitObject); + double objectStrain = getNextSingleKeyStamina(hitObject).StrainValueOf(hitObject); + // objectStrain *= ColourEvaluator.EvaluateDifficultyOf(current) * 0.3 + 0.7; + return objectStrain; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 4dd3b0b8cc..423903db2f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -20,8 +20,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyCalculator : DifficultyCalculator { - private const double rhythm_skill_multiplier = 0.014; - private const double colour_skill_multiplier = 0.01; + private const double rhythm_skill_multiplier = 0.017; + private const double colour_skill_multiplier = 0.028; private const double stamina_skill_multiplier = 0.021; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Find repetition interval for the final TaikoDifficultyHitObjectColour // TODO: Might be a good idea to refactor this - ((TaikoDifficultyHitObject)difficultyHitObject.Last()).Colour.FindRepetitionInterval(); + ((TaikoDifficultyHitObject)difficultyHitObject.Last()).Colour?.FindRepetitionInterval(); return difficultyHitObject; } @@ -76,18 +76,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier; - double staminaPenalty = simpleColourPenalty(staminaRating, colourRating); - staminaRating *= staminaPenalty; + // double staminaPenalty = simpleColourPenalty(staminaRating, colourRating); + // staminaRating *= staminaPenalty; //TODO : This is a temporary fix for the stamina rating of converts, due to their low colour variance. - if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0 && colourRating < 0.05) - { - staminaPenalty *= 0.25; - } + // if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0 && colourRating < 0.05) + // { + // staminaPenalty *= 0.25; + // } - double combinedRating = locallyCombinedDifficulty(colour, rhythm, stamina, staminaPenalty); + double combinedRating = locallyCombinedDifficulty(colour, rhythm, stamina); double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating); - double starRating = 1.4 * separatedRating + 0.5 * combinedRating; + double starRating = 1.9 * combinedRating; starRating = rescale(starRating); HitWindows hitWindows = new TaikoHitWindows(); @@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). /// - private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina stamina, double staminaPenalty) + private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina stamina) { List peaks = new List(); @@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { double colourPeak = colourPeaks[i] * colour_skill_multiplier; double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; - double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * staminaPenalty; + double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; double peak = norm(2, colourPeak, rhythmPeak, staminaPeak); From 07d3a7bd2ecd9dd585539285a2fb374847929ebf Mon Sep 17 00:00:00 2001 From: vun Date: Mon, 6 Jun 2022 16:11:26 +0800 Subject: [PATCH 008/532] Fix logic error, minor stamina changes --- .../Difficulty/Evaluators/StaminaEvaluator.cs | 6 ++-- .../Preprocessing/TaikoDifficultyHitObject.cs | 29 ++++++++++++++----- .../Difficulty/Skills/Stamina.cs | 4 +-- .../Difficulty/TaikoDifficultyCalculator.cs | 11 ++++--- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index 6f141f50ae..0c33a952a5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// The duration between the current and previous note hit using the same key. private static double speedBonus(double notePairDuration) { - return 175 / (notePairDuration + 100); + return Math.Pow(0.4, notePairDuration / 1000); } /// @@ -38,7 +39,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return 0.0; } - double objectStrain = 0.5; + double objectStrain = 0; + // Console.WriteLine(speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime)); objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime); return objectStrain; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 0949b8349a..cb57c7bccc 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; @@ -15,13 +16,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public class TaikoDifficultyHitObject : DifficultyHitObject { - // TODO: Review this - these was originally handled in TaikodifficultyCalculator.CreateDifficultyHitObjects, but - // it might be a good idea to encapsulate as much detail within the class as possible. - private static List centreHitObjects = new List(); - private static List rimHitObjects = new List(); - private readonly IReadOnlyList monoDifficultyHitObjects; public readonly int MonoPosition; + private readonly IReadOnlyList noteObjects; + public readonly int NotePosition; /// /// The rhythm required to hit this hit object. @@ -46,12 +44,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// The gameplay preceding . /// The gameplay preceding . /// The rate of the gameplay clock. Modified by speed-changing mods. - /// The list of s in the current beatmap. - /// /// The position of this in the list. - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, List objects, int position) + /// The list of all s in the current beatmap. + /// The list of centre (don) s in the current beatmap. + /// The list of rim (kat) s in the current beatmap. + /// The list of s that is a hit (i.e. not a slider or spinner) in the current beatmap. + /// The position of this in the list. + /// + /// TODO: This argument list is getting long, we might want to refactor this into a static method that create + /// all s from a . + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, + List objects, List centreHitObjects, List rimHitObjects, + List noteObjects, int position) : base(hitObject, lastObject, clockRate, objects, position) { var currentHit = hitObject as Hit; + this.noteObjects = noteObjects; Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate); HitType = currentHit?.Type; @@ -68,6 +75,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } Colour = TaikoDifficultyHitObjectColour.GetInstanceFor(this); + this.NotePosition = noteObjects.Count(); + noteObjects.Add(this); } if (HitType == Objects.HitType.Centre) @@ -124,5 +133,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public TaikoDifficultyHitObject PreviousMono(int backwardsIndex) => monoDifficultyHitObjects.ElementAtOrDefault(MonoPosition - (backwardsIndex + 1)); public TaikoDifficultyHitObject NextMono(int forwardsIndex) => monoDifficultyHitObjects.ElementAtOrDefault(MonoPosition + (forwardsIndex + 1)); + + public TaikoDifficultyHitObject PreviousNote(int backwardsIndex) => noteObjects.ElementAtOrDefault(NotePosition - (backwardsIndex + 1)); + + public TaikoDifficultyHitObject NextNote(int forwardsIndex) => noteObjects.ElementAtOrDefault(NotePosition + (forwardsIndex + 1)); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 713944a6e0..ee5e257811 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// public class Stamina : StrainDecaySkill { - protected override double SkillMultiplier => 3.6; - protected override double StrainDecayBase => 0; + protected override double SkillMultiplier => 1.2; + protected override double StrainDecayBase => 0.4; /// /// Creates a skill. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 423903db2f..187fa4a070 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -21,8 +21,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public class TaikoDifficultyCalculator : DifficultyCalculator { private const double rhythm_skill_multiplier = 0.017; - private const double colour_skill_multiplier = 0.028; - private const double stamina_skill_multiplier = 0.021; + private const double colour_skill_multiplier = 0.027; + private const double stamina_skill_multiplier = 0.017; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) @@ -47,13 +47,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { List difficultyHitObject = new List(); + List centreObjects = new List(); + List rimObjects = new List(); + List noteObjects = new List(); for (int i = 2; i < beatmap.HitObjects.Count; i++) { difficultyHitObject.Add( new TaikoDifficultyHitObject( - beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObject, difficultyHitObject.Count - ) + beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObject, + centreObjects, rimObjects, noteObjects, difficultyHitObject.Count) ); } From fd49a27cf99729b9e463ad34ea85082be789550b Mon Sep 17 00:00:00 2001 From: vun Date: Tue, 7 Jun 2022 13:30:24 +0800 Subject: [PATCH 009/532] Fix encoding repetition, parameter adjustments --- .../Difficulty/Evaluators/ColourEvaluator.cs | 8 +++--- .../Preprocessing/TaikoDifficultyHitObject.cs | 26 +++++++------------ .../TaikoDifficultyHitObjectColour.cs | 9 +++---- .../Difficulty/TaikoDifficultyCalculator.cs | 6 ++--- 4 files changed, 20 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 159f9a4508..c534b5bd9f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -16,16 +16,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; TaikoDifficultyHitObjectColour colour = taikoCurrent.Colour; if (colour == null) return 0; - double objectStrain = 1; + double objectStrain = 1.6; if (colour.Delta) { - objectStrain /= Math.Pow(colour.DeltaRunLength, 0.25); + objectStrain /= Math.Pow(colour.DeltaRunLength, 0.7); } else { - objectStrain *= sigmoid(colour.DeltaRunLength, 3, 3) * 0.3 + 0.3; + objectStrain *= sigmoid(colour.DeltaRunLength, 4, 4) * 0.5 + 0.5; } - objectStrain *= -sigmoid(colour.RepetitionInterval, 8, 8); + objectStrain *= -sigmoid(colour.RepetitionInterval, 8, 8) * 0.5 + 0.5; // Console.WriteLine($"{current.StartTime},{colour.GetHashCode()},{colour.Delta},{colour.DeltaRunLength},{colour.RepetitionInterval},{objectStrain}"); return objectStrain; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index cb57c7bccc..537eafe396 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -63,22 +63,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate); HitType = currentHit?.Type; - // Need to be done after HitType is set. - if (HitType != null) - { - // Get previous hit object, while skipping one that does not have defined colour (sliders and spinners). - // Without skipping through these, sliders and spinners would have contributed to a colour change for the next note. - TaikoDifficultyHitObject previousHitObject = (TaikoDifficultyHitObject)objects.LastOrDefault(); - while (previousHitObject != null && previousHitObject.Colour == null) - { - previousHitObject = (TaikoDifficultyHitObject)previousHitObject.Previous(0); - } - - Colour = TaikoDifficultyHitObjectColour.GetInstanceFor(this); - this.NotePosition = noteObjects.Count(); - noteObjects.Add(this); - } - if (HitType == Objects.HitType.Centre) { MonoPosition = centreHitObjects.Count(); @@ -91,6 +75,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing rimHitObjects.Add(this); monoDifficultyHitObjects = rimHitObjects; } + + // Need to be done after HitType is set. + if (HitType != null) + { + this.NotePosition = noteObjects.Count(); + noteObjects.Add(this); + + // Need to be done after NotePosition is set. + Colour = TaikoDifficultyHitObjectColour.GetInstanceFor(this); + } } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs index ce65fd0552..f9a586f877 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public static TaikoDifficultyHitObjectColour GetInstanceFor(TaikoDifficultyHitObject hitObject) { - TaikoDifficultyHitObject lastObject = (TaikoDifficultyHitObject) hitObject.Previous(0); + TaikoDifficultyHitObject lastObject = hitObject.PreviousNote(0); TaikoDifficultyHitObjectColour previous = lastObject?.Colour; bool delta = lastObject == null || hitObject.HitType != lastObject.HitType; if (previous != null && delta == previous.Delta) @@ -75,15 +75,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing TaikoDifficultyHitObjectColour other = this.previous.previous; while (other != null && interval < max_repetition_interval) { + interval += other.DeltaRunLength; if (other.Delta == this.Delta && other.DeltaRunLength == this.DeltaRunLength) { - this.RepetitionInterval = Math.Max(interval, max_repetition_interval); + this.RepetitionInterval = Math.Min(interval, max_repetition_interval); return; } - else - { - interval += other.DeltaRunLength; - } other = other.previous; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 187fa4a070..706fde77d2 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -21,8 +21,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public class TaikoDifficultyCalculator : DifficultyCalculator { private const double rhythm_skill_multiplier = 0.017; - private const double colour_skill_multiplier = 0.027; - private const double stamina_skill_multiplier = 0.017; + private const double colour_skill_multiplier = 0.026; + private const double stamina_skill_multiplier = 0.018; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // } double combinedRating = locallyCombinedDifficulty(colour, rhythm, stamina); - double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating); + // double separatedRating = norm(2, colourRating, rhythmRating, staminaRating); double starRating = 1.9 * combinedRating; starRating = rescale(starRating); From d8d4ac431e804bfa8d644e1ca78c58591bfbe7a3 Mon Sep 17 00:00:00 2001 From: vun Date: Wed, 8 Jun 2022 13:24:51 +0800 Subject: [PATCH 010/532] Refactor LocallyCombinedDifficulty to an external skill --- .../Difficulty/Skills/CombinedStrain.cs | 86 +++++++++++++++ .../Difficulty/TaikoDifficultyCalculator.cs | 101 ++---------------- 2 files changed, 97 insertions(+), 90 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs new file mode 100644 index 0000000000..e5052c3359 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Skills +{ + public class CombinedStrain : Skill + { + private const double rhythm_skill_multiplier = 0.017; + private const double colour_skill_multiplier = 0.026; + private const double stamina_skill_multiplier = 0.018; + + private Rhythm rhythm; + private Colour colour; + private Stamina stamina; + + public double ColourDifficultyValue => colour.DifficultyValue() * colour_skill_multiplier; + public double RhythmDifficultyValue => rhythm.DifficultyValue() * rhythm_skill_multiplier; + public double StaminaDifficultyValue => stamina.DifficultyValue() * stamina_skill_multiplier; + + public CombinedStrain(Mod[] mods) : base(mods) + { + rhythm = new Rhythm(mods); + colour = new Colour(mods); + stamina = new Stamina(mods); + } + + /// + /// Returns the p-norm of an n-dimensional vector. + /// + /// The value of p to calculate the norm for. + /// The coefficients of the vector. + private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); + + public override void Process(DifficultyHitObject current) + { + rhythm.Process(current); + colour.Process(current); + stamina.Process(current); + } + + /// + /// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map. + /// + /// + /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. + /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). + /// + public override double DifficultyValue() + { + List peaks = new List(); + + var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); + var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList(); + var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList(); + + for (int i = 0; i < colourPeaks.Count; i++) + { + double colourPeak = colourPeaks[i] * colour_skill_multiplier; + double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; + double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; + + double peak = norm(2, colourPeak, rhythmPeak, staminaPeak); + + // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). + // These sections will not contribute to the difficulty. + if (peak > 0) + peaks.Add(peak); + } + + double difficulty = 0; + double weight = 1; + + foreach (double strain in peaks.OrderByDescending(d => d)) + { + difficulty += strain * weight; + weight *= 0.9; + } + + return difficulty; + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 706fde77d2..f4a23930b3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -20,21 +20,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyCalculator : DifficultyCalculator { - private const double rhythm_skill_multiplier = 0.017; - private const double colour_skill_multiplier = 0.026; - private const double stamina_skill_multiplier = 0.018; - public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { } - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { - new Colour(mods), - new Rhythm(mods), - new Stamina(mods) - }; + return new Skill[] + { + new CombinedStrain(mods) + }; + } protected override Mod[] DifficultyAdjustmentMods => new Mod[] { @@ -71,27 +68,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods }; - var colour = (Colour)skills[0]; - var rhythm = (Rhythm)skills[1]; - var stamina = (Stamina)skills[2]; + var combined = (CombinedStrain)skills[0]; - double colourRating = colour.DifficultyValue() * colour_skill_multiplier; - double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; - double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier; + double colourRating = combined.ColourDifficultyValue; + double rhythmRating = combined.RhythmDifficultyValue; + double staminaRating = combined.StaminaDifficultyValue; - // double staminaPenalty = simpleColourPenalty(staminaRating, colourRating); - // staminaRating *= staminaPenalty; - - //TODO : This is a temporary fix for the stamina rating of converts, due to their low colour variance. - // if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0 && colourRating < 0.05) - // { - // staminaPenalty *= 0.25; - // } - - double combinedRating = locallyCombinedDifficulty(colour, rhythm, stamina); - // double separatedRating = norm(2, colourRating, rhythmRating, staminaRating); - double starRating = 1.9 * combinedRating; - starRating = rescale(starRating); + double starRating = rescale(1.9 * combined.DifficultyValue()); HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); @@ -108,68 +91,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty }; } - /// - /// Calculates the penalty for the stamina skill for maps with low colour difficulty. - /// - /// - /// Some maps (especially converts) can be easy to read despite a high note density. - /// This penalty aims to reduce the star rating of such maps by factoring in colour difficulty to the stamina skill. - /// - private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty) - { - if (colorDifficulty <= 0) return 0.79 - 0.25; - - return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; - } - - /// - /// Returns the p-norm of an n-dimensional vector. - /// - /// The value of p to calculate the norm for. - /// The coefficients of the vector. - private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); - - /// - /// Returns the partial star rating of the beatmap, calculated using peak strains from all sections of the map. - /// - /// - /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. - /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). - /// - private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina stamina) - { - List peaks = new List(); - - var colourPeaks = colour.GetCurrentStrainPeaks().ToList(); - var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList(); - var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList(); - - for (int i = 0; i < colourPeaks.Count; i++) - { - double colourPeak = colourPeaks[i] * colour_skill_multiplier; - double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; - double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; - - double peak = norm(2, colourPeak, rhythmPeak, staminaPeak); - - // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). - // These sections will not contribute to the difficulty. - if (peak > 0) - peaks.Add(peak); - } - - double difficulty = 0; - double weight = 1; - - foreach (double strain in peaks.OrderByDescending(d => d)) - { - difficulty += strain * weight; - weight *= 0.9; - } - - return difficulty; - } - /// /// Applies a final re-scaling of the star rating to bring maps with recorded full combos below 9.5 stars. /// From 5793ca5534e9a9d17f9e2bc94581051fbf082012 Mon Sep 17 00:00:00 2001 From: vun Date: Thu, 9 Jun 2022 12:35:26 +0800 Subject: [PATCH 011/532] Parameter tweaks --- .../Difficulty/Evaluators/ColourEvaluator.cs | 6 +++--- .../Difficulty/Skills/CombinedStrain.cs | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index c534b5bd9f..2ecef3690b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -16,14 +16,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; TaikoDifficultyHitObjectColour colour = taikoCurrent.Colour; if (colour == null) return 0; - double objectStrain = 1.6; + double objectStrain = 1.8; if (colour.Delta) { - objectStrain /= Math.Pow(colour.DeltaRunLength, 0.7); + objectStrain *= sigmoid(colour.DeltaRunLength, 6, 4) * 0.5 + 0.5; } else { - objectStrain *= sigmoid(colour.DeltaRunLength, 4, 4) * 0.5 + 0.5; + objectStrain *= sigmoid(colour.DeltaRunLength, 2, 2) * 0.5 + 0.5; } objectStrain *= -sigmoid(colour.RepetitionInterval, 8, 8) * 0.5 + 0.5; // Console.WriteLine($"{current.StartTime},{colour.GetHashCode()},{colour.Delta},{colour.DeltaRunLength},{colour.RepetitionInterval},{objectStrain}"); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs index e5052c3359..bf62cb1fbd 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs @@ -9,9 +9,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class CombinedStrain : Skill { - private const double rhythm_skill_multiplier = 0.017; - private const double colour_skill_multiplier = 0.026; - private const double stamina_skill_multiplier = 0.018; + private const double final_multiplier = 0.00925; + private const double rhythm_skill_multiplier = 1.6 * final_multiplier; + private const double colour_skill_multiplier = 1.85 * final_multiplier; + private const double stamina_skill_multiplier = 1.85 * final_multiplier; private Rhythm rhythm; private Colour colour; @@ -63,7 +64,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; - double peak = norm(2, colourPeak, rhythmPeak, staminaPeak); + double peak = norm(1.5, colourPeak, staminaPeak); + peak = norm(2, peak, rhythmPeak); // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // These sections will not contribute to the difficulty. From 6dbaf0a03084e07ec8c287c7d739bee4453384b2 Mon Sep 17 00:00:00 2001 From: Jay L Date: Thu, 9 Jun 2022 19:22:55 +1000 Subject: [PATCH 012/532] Refactor --- .../Difficulty/Evaluators/ColourEvaluator.cs | 7 ++- .../Difficulty/Evaluators/StaminaEvaluator.cs | 1 + .../Preprocessing/TaikoDifficultyHitObject.cs | 23 +++++----- .../TaikoDifficultyHitObjectColour.cs | 44 +++++++++---------- .../Difficulty/Skills/Colour.cs | 3 -- .../Skills/{CombinedStrain.cs => Peaks.cs} | 16 ++++--- .../Difficulty/Skills/Stamina.cs | 2 - .../Difficulty/TaikoDifficultyAttributes.cs | 9 ++-- .../Difficulty/TaikoDifficultyCalculator.cs | 16 ++++--- .../Difficulty/TaikoPerformanceCalculator.cs | 28 ++++++------ 10 files changed, 76 insertions(+), 73 deletions(-) rename osu.Game.Rulesets.Taiko/Difficulty/Skills/{CombinedStrain.cs => Peaks.cs} (94%) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 2ecef3690b..01410af459 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -6,6 +6,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { public class ColourEvaluator { + // TODO - Share this sigmoid private static double sigmoid(double val, double center, double width) { return Math.Tanh(Math.E * -(val - center) / width); @@ -16,7 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; TaikoDifficultyHitObjectColour colour = taikoCurrent.Colour; if (colour == null) return 0; + double objectStrain = 1.8; + if (colour.Delta) { objectStrain *= sigmoid(colour.DeltaRunLength, 6, 4) * 0.5 + 0.5; @@ -25,9 +28,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { objectStrain *= sigmoid(colour.DeltaRunLength, 2, 2) * 0.5 + 0.5; } + objectStrain *= -sigmoid(colour.RepetitionInterval, 8, 8) * 0.5 + 0.5; - // Console.WriteLine($"{current.StartTime},{colour.GetHashCode()},{colour.Delta},{colour.DeltaRunLength},{colour.RepetitionInterval},{objectStrain}"); return objectStrain; } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index 0c33a952a5..6c0c01cb2d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // Find the previous hit object hit by the current key, which is two notes of the same colour prior. TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; TaikoDifficultyHitObject keyPrevious = taikoCurrent.PreviousMono(1); + if (keyPrevious == null) { // There is no previous hit object hit by the current key diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 537eafe396..54e314f722 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -53,8 +53,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// TODO: This argument list is getting long, we might want to refactor this into a static method that create /// all s from a . public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, - List objects, List centreHitObjects, List rimHitObjects, - List noteObjects, int position) + List objects, + List centreHitObjects, + List rimHitObjects, + List noteObjects, int position) : base(hitObject, lastObject, clockRate, objects, position) { var currentHit = hitObject as Hit; @@ -65,26 +67,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (HitType == Objects.HitType.Centre) { - MonoPosition = centreHitObjects.Count(); + MonoPosition = centreHitObjects.Count; centreHitObjects.Add(this); monoDifficultyHitObjects = centreHitObjects; } else if (HitType == Objects.HitType.Rim) { - MonoPosition = rimHitObjects.Count(); + MonoPosition = rimHitObjects.Count; rimHitObjects.Add(this); monoDifficultyHitObjects = rimHitObjects; } // Need to be done after HitType is set. - if (HitType != null) - { - this.NotePosition = noteObjects.Count(); - noteObjects.Add(this); + if (HitType == null) return; - // Need to be done after NotePosition is set. - Colour = TaikoDifficultyHitObjectColour.GetInstanceFor(this); - } + NotePosition = noteObjects.Count; + noteObjects.Add(this); + + // Need to be done after NotePosition is set. + Colour = TaikoDifficultyHitObjectColour.GetInstanceFor(this); } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs index f9a586f877..a5ca0964df 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs @@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public class TaikoDifficultyHitObjectColour { - const int max_repetition_interval = 16; + private const int max_repetition_interval = 16; private TaikoDifficultyHitObjectColour previous; @@ -38,54 +38,54 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing TaikoDifficultyHitObject lastObject = hitObject.PreviousNote(0); TaikoDifficultyHitObjectColour previous = lastObject?.Colour; bool delta = lastObject == null || hitObject.HitType != lastObject.HitType; + if (previous != null && delta == previous.Delta) { previous.DeltaRunLength += 1; return previous; } - else - { - // Calculate RepetitionInterval for previous - previous?.FindRepetitionInterval(); - return new TaikoDifficultyHitObjectColour() - { - Delta = delta, - DeltaRunLength = 1, - RepetitionInterval = max_repetition_interval + 1, - previous = previous - }; - } + // Calculate RepetitionInterval for previous + previous?.FindRepetitionInterval(); + + return new TaikoDifficultyHitObjectColour() + { + Delta = delta, + DeltaRunLength = 1, + RepetitionInterval = max_repetition_interval + 1, + previous = previous + }; } /// - /// Finds the closest previous that has the identical delta value + /// Finds the closest previous that has the identical delta value /// and run length with the current instance, and returns the amount of notes between them. /// public void FindRepetitionInterval() { - if (this.previous == null || this.previous.previous == null) + if (previous?.previous == null) { - this.RepetitionInterval = max_repetition_interval + 1; + RepetitionInterval = max_repetition_interval + 1; return; } + int interval = previous.DeltaRunLength; + TaikoDifficultyHitObjectColour other = previous.previous; - int interval = this.previous.DeltaRunLength; - TaikoDifficultyHitObjectColour other = this.previous.previous; while (other != null && interval < max_repetition_interval) { interval += other.DeltaRunLength; - if (other.Delta == this.Delta && other.DeltaRunLength == this.DeltaRunLength) + + if (other.Delta == Delta && other.DeltaRunLength == DeltaRunLength) { - this.RepetitionInterval = Math.Min(interval, max_repetition_interval); + RepetitionInterval = Math.Min(interval, max_repetition_interval); return; } other = other.previous; } - this.RepetitionInterval = max_repetition_interval + 1; + RepetitionInterval = max_repetition_interval + 1; } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 9a8b350e22..1c992df179 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -1,13 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; -using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs similarity index 94% rename from osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs index bf62cb1fbd..4d4089cba7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs @@ -7,22 +7,24 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { - public class CombinedStrain : Skill + public class Peaks : Skill { - private const double final_multiplier = 0.00925; private const double rhythm_skill_multiplier = 1.6 * final_multiplier; private const double colour_skill_multiplier = 1.85 * final_multiplier; private const double stamina_skill_multiplier = 1.85 * final_multiplier; - private Rhythm rhythm; - private Colour colour; - private Stamina stamina; + private const double final_multiplier = 0.00925; + + private readonly Rhythm rhythm; + private readonly Colour colour; + private readonly Stamina stamina; public double ColourDifficultyValue => colour.DifficultyValue() * colour_skill_multiplier; public double RhythmDifficultyValue => rhythm.DifficultyValue() * rhythm_skill_multiplier; public double StaminaDifficultyValue => stamina.DifficultyValue() * stamina_skill_multiplier; - public CombinedStrain(Mod[] mods) : base(mods) + public Peaks(Mod[] mods) + : base(mods) { rhythm = new Rhythm(mods); colour = new Colour(mods); @@ -85,4 +87,4 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return difficulty; } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index ee5e257811..57c82bf97b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -5,8 +5,6 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 3dc5438072..c7342554da 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -28,13 +28,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public double ColourDifficulty { get; set; } /// - /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). + /// The difficulty corresponding to the hardest parts of the map. /// - /// - /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing. - /// - [JsonProperty("approach_rate")] - public double ApproachRate { get; set; } + [JsonProperty("peak_difficulty")] + public double PeakDifficulty { get; set; } /// /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index f4a23930b3..91ae1c4ed2 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyCalculator : DifficultyCalculator { + private const double difficulty_multiplier = 1.9; + public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) { @@ -29,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { return new Skill[] { - new CombinedStrain(mods) + new Peaks(mods) }; } @@ -68,13 +70,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods }; - var combined = (CombinedStrain)skills[0]; + var combined = (Peaks)skills[0]; - double colourRating = combined.ColourDifficultyValue; - double rhythmRating = combined.RhythmDifficultyValue; - double staminaRating = combined.StaminaDifficultyValue; + double colourRating = Math.Sqrt(combined.ColourDifficultyValue * difficulty_multiplier); + double rhythmRating = Math.Sqrt(combined.RhythmDifficultyValue * difficulty_multiplier); + double staminaRating = Math.Sqrt(combined.StaminaDifficultyValue * difficulty_multiplier); - double starRating = rescale(1.9 * combined.DifficultyValue()); + double combinedRating = combined.DifficultyValue(); + double starRating = rescale(combinedRating * difficulty_multiplier); HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); @@ -86,6 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty StaminaDifficulty = staminaRating, RhythmDifficulty = rhythmRating, ColourDifficulty = colourRating, + PeakDifficulty = combinedRating, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), }; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 8d99fd3b87..f551d8cd93 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -33,21 +33,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); - double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things - - if (score.Mods.Any(m => m is ModNoFail)) - multiplier *= 0.90; - - if (score.Mods.Any(m => m is ModHidden)) - multiplier *= 1.10; - double difficultyValue = computeDifficultyValue(score, taikoAttributes); double accuracyValue = computeAccuracyValue(score, taikoAttributes); double totalValue = Math.Pow( Math.Pow(difficultyValue, 1.1) + Math.Pow(accuracyValue, 1.1), 1.0 / 1.1 - ) * multiplier; + ) * 1.1; return new TaikoPerformanceAttributes { @@ -59,7 +51,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) { - double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.175) - 4.0, 2.25) / 450.0; + double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.190) - 4.0, 2.25) / 450.0; double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); difficultyValue *= lengthBonus; @@ -67,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty difficultyValue *= Math.Pow(0.985, countMiss); if (score.Mods.Any(m => m is ModHidden)) - difficultyValue *= 1.025; + difficultyValue *= 1.125; if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= 1.05 * lengthBonus; @@ -80,10 +72,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (attributes.GreatHitWindow <= 0) return 0; - double accValue = Math.Pow(150.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 15) * 22.0; + double accuracyValue = Math.Pow(150.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 15) * 40.0; - // Bonus for many objects - it's harder to keep good accuracy up for longer - return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); + double accuracylengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); + accuracyValue *= accuracylengthBonus; + + if (score.Mods.Any(m => m is ModHidden)) + accuracyValue *= 1.225; + + if (score.Mods.Any(m => m is ModFlashlight)) + accuracyValue *= 1.15 * accuracylengthBonus; + + return accuracyValue; } private int totalHits => countGreat + countOk + countMeh + countMiss; From 4c574eb044ed64b595145169bb1a6d8a561d028e Mon Sep 17 00:00:00 2001 From: vun Date: Thu, 9 Jun 2022 17:31:54 +0800 Subject: [PATCH 013/532] Rescale multipliers (values unaffected) --- .../Difficulty/Skills/CombinedStrain.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs index bf62cb1fbd..265f526367 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/CombinedStrain.cs @@ -9,10 +9,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class CombinedStrain : Skill { - private const double final_multiplier = 0.00925; - private const double rhythm_skill_multiplier = 1.6 * final_multiplier; - private const double colour_skill_multiplier = 1.85 * final_multiplier; - private const double stamina_skill_multiplier = 1.85 * final_multiplier; + private const double final_multiplier = 0.04625; + private const double rhythm_skill_multiplier = 0.32 * final_multiplier; + private const double colour_skill_multiplier = 0.37 * final_multiplier; + private const double stamina_skill_multiplier = 0.37 * final_multiplier; private Rhythm rhythm; private Colour colour; From 2881406f6b2912c708a186f1de0e1ebc4f0739dc Mon Sep 17 00:00:00 2001 From: vun Date: Thu, 9 Jun 2022 19:41:59 +0800 Subject: [PATCH 014/532] Nerf alternating pattern slightly, value rescale --- .../Difficulty/Evaluators/ColourEvaluator.cs | 5 +++-- osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 01410af459..8c225c8459 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -18,11 +18,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators TaikoDifficultyHitObjectColour colour = taikoCurrent.Colour; if (colour == null) return 0; - double objectStrain = 1.8; + double objectStrain = 2.1; if (colour.Delta) { - objectStrain *= sigmoid(colour.DeltaRunLength, 6, 4) * 0.5 + 0.5; + objectStrain *= sigmoid(colour.DeltaRunLength, 4, 4) * 0.5 + 0.5; } else { @@ -30,6 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators } objectStrain *= -sigmoid(colour.RepetitionInterval, 8, 8) * 0.5 + 0.5; + // Console.WriteLine($"{current.StartTime},{colour.Delta},{colour.RepetitionInterval},{objectStrain}"); return objectStrain; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs index d11aaef230..171278b4dd 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private const double colour_skill_multiplier = 0.37 * final_multiplier; private const double stamina_skill_multiplier = 0.37 * final_multiplier; - private const double final_multiplier = 0.04625; + private const double final_multiplier = 0.047; private readonly Rhythm rhythm; private readonly Colour colour; From 2b2150ac04d93e86ba285a52fda237160f76d6a9 Mon Sep 17 00:00:00 2001 From: vun Date: Fri, 10 Jun 2022 14:58:50 +0800 Subject: [PATCH 015/532] Refactor TaikoDifficultyHitObject creation into the class as a static method --- .../Preprocessing/TaikoDifficultyHitObject.cs | 32 ++++++++++++++++++- .../Difficulty/TaikoDifficultyCalculator.cs | 19 +---------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 54e314f722..1da4a6e994 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -37,6 +37,36 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public readonly HitType? HitType; + /// + /// Creates a list of s from a s. + /// TODO: Review this - this is moved here from TaikoDifficultyCalculator so that TaikoDifficultyCalculator can + /// have less knowledge of implementation details (i.e. creating all the different hitObject lists, and + /// calling FindRepetitionInterval for the final object). The down side of this is + /// TaikoDifficultyHitObejct.CreateDifficultyHitObjects is now pretty much a proxy for this. + /// + /// The beatmap from which the list of is created. + /// The rate at which the gameplay clock is run at. + public static List Create(IBeatmap beatmap, double clockRate) + { + List difficultyHitObject = new List(); + List centreObjects = new List(); + List rimObjects = new List(); + List noteObjects = new List(); + + for (int i = 2; i < beatmap.HitObjects.Count; i++) + { + difficultyHitObject.Add( + new TaikoDifficultyHitObject( + beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObject, + centreObjects, rimObjects, noteObjects, difficultyHitObject.Count) + ); + } + + // Find repetition interval for the final TaikoDifficultyHitObjectColour + ((TaikoDifficultyHitObject)difficultyHitObject.Last()).Colour?.FindRepetitionInterval(); + return difficultyHitObject; + } + /// /// Creates a new difficulty hit object. /// @@ -52,7 +82,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// /// TODO: This argument list is getting long, we might want to refactor this into a static method that create /// all s from a . - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, + private TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, List objects, List centreHitObjects, List rimHitObjects, diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 91ae1c4ed2..7898b7994e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -45,24 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - List difficultyHitObject = new List(); - List centreObjects = new List(); - List rimObjects = new List(); - List noteObjects = new List(); - - for (int i = 2; i < beatmap.HitObjects.Count; i++) - { - difficultyHitObject.Add( - new TaikoDifficultyHitObject( - beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObject, - centreObjects, rimObjects, noteObjects, difficultyHitObject.Count) - ); - } - - // Find repetition interval for the final TaikoDifficultyHitObjectColour - // TODO: Might be a good idea to refactor this - ((TaikoDifficultyHitObject)difficultyHitObject.Last()).Colour?.FindRepetitionInterval(); - return difficultyHitObject; + return TaikoDifficultyHitObject.Create(beatmap, clockRate); } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) From a5bf16e87345d488ff5f95578002a9be29276f47 Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Sun, 19 Jun 2022 02:10:23 +0200 Subject: [PATCH 016/532] Make drum rolls and swells optional with `Classic` mod --- .../Judgements/TaikoDrumRollJudgement.cs | 4 ++++ .../Judgements/TaikoDrumRollTickJudgement.cs | 4 +++- .../Judgements/TaikoSwellJudgement.cs | 4 ++++ .../Mods/TaikoModClassic.cs | 11 ++++++++++- .../Objects/Drawables/DrawableDrumRoll.cs | 9 ++++++++- .../Objects/Drawables/DrawableSwell.cs | 11 +++++++++-- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 18 +++++++++++++++++- .../Objects/DrumRollTick.cs | 7 ++++++- osu.Game.Rulesets.Taiko/Objects/Swell.cs | 12 +++++++++++- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 9 +++++++++ 10 files changed, 81 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs index be128d85b5..93891769ec 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs @@ -9,6 +9,10 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoDrumRollJudgement : TaikoJudgement { + public bool IsBonus = false; + + public override HitResult MaxResult => IsBonus ? HitResult.LargeBonus : HitResult.Great; + protected override double HealthIncreaseFor(HitResult result) { // Drum rolls can be ignored with no health penalty diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs index 5f2587a5d5..d6fb8dc2c9 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs @@ -9,7 +9,9 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoDrumRollTickJudgement : TaikoJudgement { - public override HitResult MaxResult => HitResult.SmallTickHit; + public bool IsBonus = false; + + public override HitResult MaxResult => IsBonus ? HitResult.LargeBonus : HitResult.SmallTickHit; protected override double HealthIncreaseFor(HitResult result) { diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs index b2ac0b7f03..a2572133f2 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs @@ -9,6 +9,10 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoSwellJudgement : TaikoJudgement { + public bool IsBonus = false; + + public override HitResult MaxResult => IsBonus ? HitResult.LargeBonus : HitResult.Great; + protected override double HealthIncreaseFor(HitResult result) { switch (result) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs index 6d1a18bb78..2564fd32ab 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs @@ -4,13 +4,14 @@ #nullable disable using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset, IUpdatableByPlayfield + public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset, IUpdatableByPlayfield, IApplicableToHitObject { private DrawableTaikoRuleset drawableTaikoRuleset; @@ -20,6 +21,14 @@ namespace osu.Game.Rulesets.Taiko.Mods drawableTaikoRuleset.LockPlayfieldAspect.Value = false; } + public void ApplyToHitObject(HitObject hitObject) + { + if (hitObject is DrumRoll drumRoll) + drumRoll.SetBonus(true); + else if (hitObject is Swell swell) + swell.SetBonus(true); + } + public void Update(Playfield playfield) { // Classic taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened. diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 04ed6d0b87..fd8b8196cd 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -144,7 +144,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (countHit >= HitObject.RequiredGoodHits) { - ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok); + ApplyResult(r => + { + // With the Classic mod, don't award points for a finished drum roll, only for ticks. + if (r.Judgement.MaxResult == HitResult.LargeBonus) + r.Type = HitResult.IgnoreMiss; + else + r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok; + }); } else ApplyResult(r => r.Type = r.Judgement.MinResult); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 2451c79772..238aa2189f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -201,7 +201,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); if (numHits == HitObject.RequiredHits) - ApplyResult(r => r.Type = HitResult.Great); + ApplyResult(r => r.Type = r.Judgement.MaxResult); } else { @@ -222,7 +222,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables tick.TriggerResult(false); } - ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : r.Judgement.MinResult); + ApplyResult(r => + { + // With the Classic mod, don't award combo or accuracy for a finished swell. + if (r.Judgement.MaxResult == HitResult.LargeBonus) + r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.LargeBonus : r.Judgement.MinResult; + else + r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : r.Judgement.MinResult; + }); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index e1619e2857..47ccb9cc72 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -52,6 +52,11 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public double RequiredGreatHits { get; protected set; } + /// + /// Defines if drum rolls are affected by the Classic mod, making them bonus only. + /// + private bool isBonus; + /// /// The length (in milliseconds) between ticks of this drumroll. /// Half of this value is the hit window of the ticks. @@ -106,7 +111,18 @@ namespace osu.Game.Rulesets.Taiko.Objects } } - public override Judgement CreateJudgement() => new TaikoDrumRollJudgement(); + public void SetBonus(bool bonus) + { + isBonus = bonus; + + foreach (HitObject hitObject in NestedHitObjects) + { + if (hitObject is DrumRollTick drumRollTick) + drumRollTick.IsBonus = bonus; + } + } + + public override Judgement CreateJudgement() => new TaikoDrumRollJudgement { IsBonus = isBonus }; protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs index 433fdab908..6aa08e7c76 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs @@ -27,7 +27,12 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public double HitWindow => TickSpacing / 2; - public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement(); + /// + /// Defines if ticks are affected by the Classic mod, making them bonus only. + /// + public bool IsBonus = false; + + public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement { IsBonus = IsBonus }; protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index cb91c46b4d..bd91fcd076 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs @@ -26,6 +26,16 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public int RequiredHits = 10; + /// + /// Defines if swells are affected by the Classic mod, making them bonus only. + /// + private bool isBonus; + + public void SetBonus(bool bonus) + { + isBonus = bonus; + } + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { base.CreateNestedHitObjects(cancellationToken); @@ -37,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Objects } } - public override Judgement CreateJudgement() => new TaikoSwellJudgement(); + public override Judgement CreateJudgement() => new TaikoSwellJudgement { IsBonus = isBonus }; protected override HitWindows CreateHitWindows() => HitWindows.Empty; } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 4e0c8029fb..7719f51f2f 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -295,6 +295,15 @@ namespace osu.Game.Rulesets.Taiko.UI break; default: + // Don't draw judgement results for bonus sliderticks with the Classic mod. + switch (result.Type) + { + case HitResult.IgnoreHit: + case HitResult.IgnoreMiss: + case HitResult.LargeBonus: + return; + } + judgementContainer.Add(judgementPools[result.Type].Get(j => { j.Apply(result, judgedObject); From da1d99d5b66aad2fb2fc8ed4fe096309784eed62 Mon Sep 17 00:00:00 2001 From: vun Date: Sun, 19 Jun 2022 17:14:31 +0800 Subject: [PATCH 017/532] Parameter tweaks, change repetition interval definition --- .../Difficulty/Evaluators/ColourEvaluator.cs | 6 +++--- .../Preprocessing/TaikoDifficultyHitObjectColour.cs | 6 ++++-- osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs | 5 +++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 8c225c8459..32bc8429b8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -18,18 +18,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators TaikoDifficultyHitObjectColour colour = taikoCurrent.Colour; if (colour == null) return 0; - double objectStrain = 2.1; + double objectStrain = 1.85; if (colour.Delta) { - objectStrain *= sigmoid(colour.DeltaRunLength, 4, 4) * 0.5 + 0.5; + objectStrain *= sigmoid(colour.DeltaRunLength, 3, 3) * 0.5 + 0.5; } else { objectStrain *= sigmoid(colour.DeltaRunLength, 2, 2) * 0.5 + 0.5; } - objectStrain *= -sigmoid(colour.RepetitionInterval, 8, 8) * 0.5 + 0.5; + objectStrain *= -sigmoid(colour.RepetitionInterval, 1, 8); // * 0.5 + 0.5; // Console.WriteLine($"{current.StartTime},{colour.Delta},{colour.RepetitionInterval},{objectStrain}"); return objectStrain; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs index a5ca0964df..8be803fd05 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs @@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public int RepetitionInterval { get; private set; } + public TaikoDifficultyHitObjectColour repeatedColour { get; private set; } + /// /// Get the instance for the given hitObject. This is implemented /// as a static function instead of constructor to allow for reusing existing instances. @@ -74,14 +76,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing while (other != null && interval < max_repetition_interval) { - interval += other.DeltaRunLength; - if (other.Delta == Delta && other.DeltaRunLength == DeltaRunLength) { RepetitionInterval = Math.Min(interval, max_repetition_interval); + repeatedColour = other; return; } + interval += other.DeltaRunLength; other = other.previous; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs index 171278b4dd..ebbe027f9e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs @@ -4,14 +4,15 @@ using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Peaks : Skill { private const double rhythm_skill_multiplier = 0.32 * final_multiplier; - private const double colour_skill_multiplier = 0.37 * final_multiplier; - private const double stamina_skill_multiplier = 0.37 * final_multiplier; + private const double colour_skill_multiplier = 0.33 * final_multiplier; + private const double stamina_skill_multiplier = 0.4 * final_multiplier; private const double final_multiplier = 0.047; From 57964311be3410c7df1b563b3fb5b1daa19a752b Mon Sep 17 00:00:00 2001 From: vun Date: Sun, 19 Jun 2022 17:20:53 +0800 Subject: [PATCH 018/532] Revert performance calculator to upstream --- .../Difficulty/TaikoPerformanceCalculator.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 9eca59bfd7..a9cde62f44 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -35,13 +35,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); + double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things + + if (score.Mods.Any(m => m is ModNoFail)) + multiplier *= 0.90; + + if (score.Mods.Any(m => m is ModHidden)) + multiplier *= 1.10; + double difficultyValue = computeDifficultyValue(score, taikoAttributes); double accuracyValue = computeAccuracyValue(score, taikoAttributes); double totalValue = Math.Pow( Math.Pow(difficultyValue, 1.1) + Math.Pow(accuracyValue, 1.1), 1.0 / 1.1 - ) * 1.1; + ) * multiplier; return new TaikoPerformanceAttributes { @@ -53,7 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) { - double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.190) - 4.0, 2.25) / 450.0; + double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.175) - 4.0, 2.25) / 450.0; double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); difficultyValue *= lengthBonus; @@ -61,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty difficultyValue *= Math.Pow(0.985, countMiss); if (score.Mods.Any(m => m is ModHidden)) - difficultyValue *= 1.125; + difficultyValue *= 1.025; if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= 1.05 * lengthBonus; @@ -74,18 +82,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (attributes.GreatHitWindow <= 0) return 0; - double accuracyValue = Math.Pow(150.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 15) * 40.0; + double accValue = Math.Pow(150.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 15) * 22.0; - double accuracylengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); - accuracyValue *= accuracylengthBonus; - - if (score.Mods.Any(m => m is ModHidden)) - accuracyValue *= 1.225; - - if (score.Mods.Any(m => m is ModFlashlight)) - accuracyValue *= 1.15 * accuracylengthBonus; - - return accuracyValue; + // Bonus for many objects - it's harder to keep good accuracy up for longer + return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); } private int totalHits => countGreat + countOk + countMeh + countMiss; From 3529514587a5099a54b9e02c6d62321dcd66444e Mon Sep 17 00:00:00 2001 From: vun Date: Sun, 19 Jun 2022 17:26:11 +0800 Subject: [PATCH 019/532] Disablle nullable in TaikoDifficultyHitObjectColour --- .../Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs index 8be803fd05..8304c0b126 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs @@ -1,3 +1,5 @@ +#nullable disable + using System; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing From 98527fec26e7d88af58edd376a6d2da0ef4f32ff Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Sun, 19 Jun 2022 15:11:12 +0200 Subject: [PATCH 020/532] Make mod selfcontained --- .../Judgements/TaikoDrumRollJudgement.cs | 4 - .../Judgements/TaikoDrumRollTickJudgement.cs | 4 +- .../Judgements/TaikoSwellJudgement.cs | 4 - .../Mods/TaikoModClassic.cs | 184 +++++++++++++++++- .../Objects/Drawables/DrawableDrumRoll.cs | 9 +- .../Objects/Drawables/DrawableSwell.cs | 50 +++-- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 32 +-- .../Objects/DrumRollTick.cs | 7 +- osu.Game.Rulesets.Taiko/Objects/Swell.cs | 12 +- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 9 - 10 files changed, 213 insertions(+), 102 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs index 93891769ec..be128d85b5 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs @@ -9,10 +9,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoDrumRollJudgement : TaikoJudgement { - public bool IsBonus = false; - - public override HitResult MaxResult => IsBonus ? HitResult.LargeBonus : HitResult.Great; - protected override double HealthIncreaseFor(HitResult result) { // Drum rolls can be ignored with no health penalty diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs index d6fb8dc2c9..5f2587a5d5 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs @@ -9,9 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoDrumRollTickJudgement : TaikoJudgement { - public bool IsBonus = false; - - public override HitResult MaxResult => IsBonus ? HitResult.LargeBonus : HitResult.SmallTickHit; + public override HitResult MaxResult => HitResult.SmallTickHit; protected override double HealthIncreaseFor(HitResult result) { diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs index a2572133f2..b2ac0b7f03 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs @@ -9,10 +9,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoSwellJudgement : TaikoJudgement { - public bool IsBonus = false; - - public override HitResult MaxResult => IsBonus ? HitResult.LargeBonus : HitResult.Great; - protected override double HealthIncreaseFor(HitResult result) { switch (result) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs index 2564fd32ab..7141afe791 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs @@ -3,15 +3,22 @@ #nullable disable +using System.Linq; +using System.Threading; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset, IUpdatableByPlayfield, IApplicableToHitObject + public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset, IUpdatableByPlayfield, IApplicableAfterBeatmapConversion { private DrawableTaikoRuleset drawableTaikoRuleset; @@ -19,14 +26,177 @@ namespace osu.Game.Rulesets.Taiko.Mods { drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; drawableTaikoRuleset.LockPlayfieldAspect.Value = false; + + drawableTaikoRuleset.Playfield.RegisterPool(5); + drawableTaikoRuleset.Playfield.RegisterPool(100); + drawableTaikoRuleset.Playfield.RegisterPool(5); } - public void ApplyToHitObject(HitObject hitObject) + public void ApplyToBeatmap(IBeatmap beatmap) { - if (hitObject is DrumRoll drumRoll) - drumRoll.SetBonus(true); - else if (hitObject is Swell swell) - swell.SetBonus(true); + var taikoBeatmap = (TaikoBeatmap)beatmap; + + if (taikoBeatmap.HitObjects.Count == 0) return; + + var hitObjects = taikoBeatmap.HitObjects.Select(ho => + { + if (ho is DrumRoll drumRoll) + { + var newDrumRoll = new ClassicDrumRoll(drumRoll); + return newDrumRoll; + } + + if (ho is Swell swell) + { + var newSwell = new ClassicSwell(swell); + return newSwell; + } + + return ho; + }).ToList(); + + taikoBeatmap.HitObjects = hitObjects; + } + + private class ClassicDrumRoll : DrumRoll + { + public ClassicDrumRoll(DrumRoll original) + { + StartTime = original.StartTime; + Samples = original.Samples; + EndTime = original.EndTime; + Duration = original.Duration; + TickRate = original.TickRate; + RequiredGoodHits = original.RequiredGoodHits; + RequiredGreatHits = original.RequiredGreatHits; + } + + public override Judgement CreateJudgement() => new TaikoClassicDrumRollJudgement(); + + protected override void CreateTicks(CancellationToken cancellationToken) + { + if (TickSpacing == 0) + return; + + bool first = true; + + for (double t = StartTime; t < EndTime + TickSpacing / 2; t += TickSpacing) + { + cancellationToken.ThrowIfCancellationRequested(); + + AddNested(new ClassicDrumRollTick + { + FirstTick = first, + TickSpacing = TickSpacing, + StartTime = t, + IsStrong = IsStrong + }); + + first = false; + } + } + } + + private class ClassicDrumRollTick : DrumRollTick + { + public override Judgement CreateJudgement() => new TaikoClassicDrumRollTickJudgement(); + } + + private class ClassicSwell : Swell + { + public ClassicSwell(Swell original) + { + StartTime = original.StartTime; + Samples = original.Samples; + EndTime = original.EndTime; + Duration = original.Duration; + RequiredHits = original.RequiredHits; + } + + public override Judgement CreateJudgement() => new TaikoClassicSwellJudgement(); + } + + private class TaikoClassicDrumRollJudgement : TaikoDrumRollJudgement + { + public override HitResult MaxResult => HitResult.LargeBonus; + } + + private class TaikoClassicDrumRollTickJudgement : TaikoDrumRollTickJudgement + { + public override HitResult MaxResult => HitResult.LargeBonus; + } + + private class TaikoClassicSwellJudgement : TaikoSwellJudgement + { + public override HitResult MaxResult => HitResult.LargeBonus; + } + + private class ClassicDrawableDrumRoll : DrawableDrumRoll + { + public override bool DisplayResult => false; + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (userTriggered) + return; + + if (timeOffset < 0) + return; + + ApplyResult(r => r.Type = HitResult.IgnoreMiss); + } + } + + private class ClassicDrawableSwell : DrawableSwell + { + public override bool DisplayResult => false; + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (userTriggered) + { + DrawableSwellTick nextTick = null; + + foreach (var t in Ticks) + { + if (!t.Result.HasResult) + { + nextTick = t; + break; + } + } + + nextTick?.TriggerResult(true); + + int numHits = Ticks.Count(r => r.IsHit); + + AnimateCompletion(numHits); + + if (numHits == HitObject.RequiredHits) + ApplyResult(r => r.Type = HitResult.LargeBonus); + } + else + { + if (timeOffset < 0) + return; + + int numHits = 0; + + foreach (var tick in Ticks) + { + if (tick.IsHit) + { + numHits++; + continue; + } + + if (!tick.Result.HasResult) + tick.TriggerResult(false); + } + + ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.SmallBonus : HitResult.IgnoreMiss); + } + } } public void Update(Playfield playfield) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index fd8b8196cd..04ed6d0b87 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -144,14 +144,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (countHit >= HitObject.RequiredGoodHits) { - ApplyResult(r => - { - // With the Classic mod, don't award points for a finished drum roll, only for ticks. - if (r.Judgement.MaxResult == HitResult.LargeBonus) - r.Type = HitResult.IgnoreMiss; - else - r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok; - }); + ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok); } else ApplyResult(r => r.Type = r.Judgement.MinResult); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 238aa2189f..34fe6c5a73 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// private const double ring_appear_offset = 100; - private readonly Container ticks; + protected readonly Container Ticks; private readonly Container bodyContainer; private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } }); - AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); + AddInternal(Ticks = new Container { RelativeSizeAxes = Axes.Both }); } [BackgroundDependencyLoader] @@ -132,6 +132,20 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Origin = Anchor.Centre, }); + protected void AnimateCompletion(int numHits) + { + float completion = (float)numHits / HitObject.RequiredHits; + + expandingRing + .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) + .Then() + .FadeTo(completion / 8, 2000, Easing.OutQuint); + + MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); + + expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + } + protected override void OnFree() { base.OnFree(); @@ -148,7 +162,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (hitObject) { case DrawableSwellTick tick: - ticks.Add(tick); + Ticks.Add(tick); break; } } @@ -156,7 +170,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - ticks.Clear(false); + Ticks.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) @@ -176,7 +190,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { DrawableSwellTick nextTick = null; - foreach (var t in ticks) + foreach (var t in Ticks) { if (!t.Result.HasResult) { @@ -187,21 +201,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables nextTick?.TriggerResult(true); - int numHits = ticks.Count(r => r.IsHit); + int numHits = Ticks.Count(r => r.IsHit); - float completion = (float)numHits / HitObject.RequiredHits; - - expandingRing - .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) - .Then() - .FadeTo(completion / 8, 2000, Easing.OutQuint); - - MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); - - expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + AnimateCompletion(numHits); if (numHits == HitObject.RequiredHits) - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(r => r.Type = HitResult.Great); } else { @@ -210,7 +215,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = 0; - foreach (var tick in ticks) + foreach (var tick in Ticks) { if (tick.IsHit) { @@ -222,14 +227,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables tick.TriggerResult(false); } - ApplyResult(r => - { - // With the Classic mod, don't award combo or accuracy for a finished swell. - if (r.Judgement.MaxResult == HitResult.LargeBonus) - r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.LargeBonus : r.Judgement.MinResult; - else - r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : r.Judgement.MinResult; - }); + ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : r.Judgement.MinResult); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 47ccb9cc72..80e8bec0cb 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -52,16 +52,11 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public double RequiredGreatHits { get; protected set; } - /// - /// Defines if drum rolls are affected by the Classic mod, making them bonus only. - /// - private bool isBonus; - /// /// The length (in milliseconds) between ticks of this drumroll. /// Half of this value is the hit window of the ticks. /// - private double tickSpacing = 100; + protected double TickSpacing = 100; private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY; @@ -74,13 +69,13 @@ namespace osu.Game.Rulesets.Taiko.Objects double scoringDistance = base_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; - tickSpacing = timingPoint.BeatLength / TickRate; + TickSpacing = timingPoint.BeatLength / TickRate; overallDifficulty = difficulty.OverallDifficulty; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - createTicks(cancellationToken); + CreateTicks(cancellationToken); RequiredGoodHits = NestedHitObjects.Count * Math.Min(0.15, 0.05 + 0.10 / 6 * overallDifficulty); RequiredGreatHits = NestedHitObjects.Count * Math.Min(0.30, 0.10 + 0.20 / 6 * overallDifficulty); @@ -88,21 +83,21 @@ namespace osu.Game.Rulesets.Taiko.Objects base.CreateNestedHitObjects(cancellationToken); } - private void createTicks(CancellationToken cancellationToken) + protected virtual void CreateTicks(CancellationToken cancellationToken) { - if (tickSpacing == 0) + if (TickSpacing == 0) return; bool first = true; - for (double t = StartTime; t < EndTime + tickSpacing / 2; t += tickSpacing) + for (double t = StartTime; t < EndTime + TickSpacing / 2; t += TickSpacing) { cancellationToken.ThrowIfCancellationRequested(); AddNested(new DrumRollTick { FirstTick = first, - TickSpacing = tickSpacing, + TickSpacing = TickSpacing, StartTime = t, IsStrong = IsStrong }); @@ -111,18 +106,7 @@ namespace osu.Game.Rulesets.Taiko.Objects } } - public void SetBonus(bool bonus) - { - isBonus = bonus; - - foreach (HitObject hitObject in NestedHitObjects) - { - if (hitObject is DrumRollTick drumRollTick) - drumRollTick.IsBonus = bonus; - } - } - - public override Judgement CreateJudgement() => new TaikoDrumRollJudgement { IsBonus = isBonus }; + public override Judgement CreateJudgement() => new TaikoDrumRollJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs index 6aa08e7c76..433fdab908 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs @@ -27,12 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public double HitWindow => TickSpacing / 2; - /// - /// Defines if ticks are affected by the Classic mod, making them bonus only. - /// - public bool IsBonus = false; - - public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement { IsBonus = IsBonus }; + public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index bd91fcd076..cb91c46b4d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs @@ -26,16 +26,6 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public int RequiredHits = 10; - /// - /// Defines if swells are affected by the Classic mod, making them bonus only. - /// - private bool isBonus; - - public void SetBonus(bool bonus) - { - isBonus = bonus; - } - protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { base.CreateNestedHitObjects(cancellationToken); @@ -47,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Objects } } - public override Judgement CreateJudgement() => new TaikoSwellJudgement { IsBonus = isBonus }; + public override Judgement CreateJudgement() => new TaikoSwellJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 7719f51f2f..4e0c8029fb 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -295,15 +295,6 @@ namespace osu.Game.Rulesets.Taiko.UI break; default: - // Don't draw judgement results for bonus sliderticks with the Classic mod. - switch (result.Type) - { - case HitResult.IgnoreHit: - case HitResult.IgnoreMiss: - case HitResult.LargeBonus: - return; - } - judgementContainer.Add(judgementPools[result.Type].Get(j => { j.Apply(result, judgedObject); From 6c8042642aaf4e04c32a98e45ee330945bc280e9 Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Mon, 20 Jun 2022 17:22:41 +0200 Subject: [PATCH 021/532] Reduce code duplication --- .../Mods/TaikoModClassic.cs | 106 +++++------------- .../Objects/Drawables/DrawableSwell.cs | 44 ++++---- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 16 ++- 3 files changed, 63 insertions(+), 103 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs index 7141afe791..704185cc3c 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs @@ -4,6 +4,7 @@ #nullable disable using System.Linq; +using System.Collections.Generic; using System.Threading; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; @@ -40,19 +41,17 @@ namespace osu.Game.Rulesets.Taiko.Mods var hitObjects = taikoBeatmap.HitObjects.Select(ho => { - if (ho is DrumRoll drumRoll) + switch (ho) { - var newDrumRoll = new ClassicDrumRoll(drumRoll); - return newDrumRoll; - } + case DrumRoll drumRoll: + return new ClassicDrumRoll(drumRoll); - if (ho is Swell swell) - { - var newSwell = new ClassicSwell(swell); - return newSwell; - } + case Swell swell: + return new ClassicSwell(swell); - return ho; + default: + return ho; + } }).ToList(); taikoBeatmap.HitObjects = hitObjects; @@ -73,33 +72,35 @@ namespace osu.Game.Rulesets.Taiko.Mods public override Judgement CreateJudgement() => new TaikoClassicDrumRollJudgement(); - protected override void CreateTicks(CancellationToken cancellationToken) + protected override List CreateTicks(CancellationToken cancellationToken) { - if (TickSpacing == 0) - return; + List oldTicks = base.CreateTicks(cancellationToken); - bool first = true; - - for (double t = StartTime; t < EndTime + TickSpacing / 2; t += TickSpacing) + List newTicks = oldTicks.Select(oldTick => { - cancellationToken.ThrowIfCancellationRequested(); - - AddNested(new ClassicDrumRollTick + if (oldTick is DrumRollTick drumRollTick) { - FirstTick = first, - TickSpacing = TickSpacing, - StartTime = t, - IsStrong = IsStrong - }); + return new ClassicDrumRollTick(drumRollTick); + } - first = false; - } + return oldTick; + }).ToList(); + + return newTicks; } } private class ClassicDrumRollTick : DrumRollTick { public override Judgement CreateJudgement() => new TaikoClassicDrumRollTickJudgement(); + + public ClassicDrumRollTick(DrumRollTick original) + { + StartTime = original.StartTime; + Samples = original.Samples; + FirstTick = original.FirstTick; + TickSpacing = original.TickSpacing; + } } private class ClassicSwell : Swell @@ -118,12 +119,12 @@ namespace osu.Game.Rulesets.Taiko.Mods private class TaikoClassicDrumRollJudgement : TaikoDrumRollJudgement { - public override HitResult MaxResult => HitResult.LargeBonus; + public override HitResult MaxResult => HitResult.IgnoreHit; } private class TaikoClassicDrumRollTickJudgement : TaikoDrumRollTickJudgement { - public override HitResult MaxResult => HitResult.LargeBonus; + public override HitResult MaxResult => HitResult.SmallBonus; } private class TaikoClassicSwellJudgement : TaikoSwellJudgement @@ -143,7 +144,7 @@ namespace osu.Game.Rulesets.Taiko.Mods if (timeOffset < 0) return; - ApplyResult(r => r.Type = HitResult.IgnoreMiss); + ApplyResult(r => r.Type = HitResult.IgnoreHit); } } @@ -151,52 +152,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override bool DisplayResult => false; - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - if (userTriggered) - { - DrawableSwellTick nextTick = null; - - foreach (var t in Ticks) - { - if (!t.Result.HasResult) - { - nextTick = t; - break; - } - } - - nextTick?.TriggerResult(true); - - int numHits = Ticks.Count(r => r.IsHit); - - AnimateCompletion(numHits); - - if (numHits == HitObject.RequiredHits) - ApplyResult(r => r.Type = HitResult.LargeBonus); - } - else - { - if (timeOffset < 0) - return; - - int numHits = 0; - - foreach (var tick in Ticks) - { - if (tick.IsHit) - { - numHits++; - continue; - } - - if (!tick.Result.HasResult) - tick.TriggerResult(false); - } - - ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.SmallBonus : HitResult.IgnoreMiss); - } - } + protected override HitResult OkResult => HitResult.SmallBonus; } public void Update(Playfield playfield) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 34fe6c5a73..5b70c59376 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -34,7 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// private const double ring_appear_offset = 100; - protected readonly Container Ticks; + protected virtual HitResult OkResult => HitResult.Ok; + private readonly Container ticks; private readonly Container bodyContainer; private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; @@ -114,7 +115,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } }); - AddInternal(Ticks = new Container { RelativeSizeAxes = Axes.Both }); + AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); } [BackgroundDependencyLoader] @@ -132,20 +133,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Origin = Anchor.Centre, }); - protected void AnimateCompletion(int numHits) - { - float completion = (float)numHits / HitObject.RequiredHits; - - expandingRing - .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) - .Then() - .FadeTo(completion / 8, 2000, Easing.OutQuint); - - MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); - - expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); - } - protected override void OnFree() { base.OnFree(); @@ -162,7 +149,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (hitObject) { case DrawableSwellTick tick: - Ticks.Add(tick); + ticks.Add(tick); break; } } @@ -170,7 +157,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - Ticks.Clear(false); + ticks.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) @@ -190,7 +177,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { DrawableSwellTick nextTick = null; - foreach (var t in Ticks) + foreach (var t in ticks) { if (!t.Result.HasResult) { @@ -201,12 +188,21 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables nextTick?.TriggerResult(true); - int numHits = Ticks.Count(r => r.IsHit); + int numHits = ticks.Count(r => r.IsHit); - AnimateCompletion(numHits); + float completion = (float)numHits / HitObject.RequiredHits; + + expandingRing + .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) + .Then() + .FadeTo(completion / 8, 2000, Easing.OutQuint); + + MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); + + expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); if (numHits == HitObject.RequiredHits) - ApplyResult(r => r.Type = HitResult.Great); + ApplyResult(r => r.Type = r.Judgement.MaxResult); } else { @@ -215,7 +211,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = 0; - foreach (var tick in Ticks) + foreach (var tick in ticks) { if (tick.IsHit) { @@ -227,7 +223,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables tick.TriggerResult(false); } - ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : r.Judgement.MinResult); + ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? OkResult : r.Judgement.MinResult); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 80e8bec0cb..5e5d9daeb1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -5,6 +5,7 @@ using osu.Game.Rulesets.Objects.Types; using System; +using System.Collections.Generic; using System.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -75,7 +76,10 @@ namespace osu.Game.Rulesets.Taiko.Objects protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - CreateTicks(cancellationToken); + foreach (TaikoHitObject tick in CreateTicks(cancellationToken)) + { + AddNested(tick); + } RequiredGoodHits = NestedHitObjects.Count * Math.Min(0.15, 0.05 + 0.10 / 6 * overallDifficulty); RequiredGreatHits = NestedHitObjects.Count * Math.Min(0.30, 0.10 + 0.20 / 6 * overallDifficulty); @@ -83,10 +87,12 @@ namespace osu.Game.Rulesets.Taiko.Objects base.CreateNestedHitObjects(cancellationToken); } - protected virtual void CreateTicks(CancellationToken cancellationToken) + protected virtual List CreateTicks(CancellationToken cancellationToken) { + List ticks = new List(); + if (TickSpacing == 0) - return; + return ticks; bool first = true; @@ -94,7 +100,7 @@ namespace osu.Game.Rulesets.Taiko.Objects { cancellationToken.ThrowIfCancellationRequested(); - AddNested(new DrumRollTick + ticks.Add(new DrumRollTick { FirstTick = first, TickSpacing = TickSpacing, @@ -104,6 +110,8 @@ namespace osu.Game.Rulesets.Taiko.Objects first = false; } + + return ticks; } public override Judgement CreateJudgement() => new TaikoDrumRollJudgement(); From c5fd48372ba8d94f39ecfb62d899474ccc32eca7 Mon Sep 17 00:00:00 2001 From: vun Date: Wed, 22 Jun 2022 17:17:19 +0800 Subject: [PATCH 022/532] Flatten speed bonus for stamina --- .../Difficulty/Evaluators/StaminaEvaluator.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index 6c0c01cb2d..be1514891c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -13,10 +13,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// /// Applies a speed bonus dependent on the time since the last hit performed using this key. /// - /// The duration between the current and previous note hit using the same key. - private static double speedBonus(double notePairDuration) + /// The interval between the current and previous note hit using the same key. + private static double speedBonus(double interval) { - return Math.Pow(0.4, notePairDuration / 1000); + return Math.Pow(0.8, interval / 1000); } /// @@ -40,9 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return 0.0; } - double objectStrain = 0; - // Console.WriteLine(speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime)); - objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime); + double objectStrain = 0.85; + objectStrain *= speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime); return objectStrain; } } From f42aac99546889a66303521674283e92e0a240bf Mon Sep 17 00:00:00 2001 From: vun Date: Thu, 23 Jun 2022 17:10:30 +0800 Subject: [PATCH 023/532] TAIKO-6 Pre-evaluate colour to avoid per-note evaluation --- .../Difficulty/Evaluators/ColourEvaluator.cs | 12 ++-- .../Preprocessing/TaikoDifficultyHitObject.cs | 21 +++--- .../TaikoDifficultyHitObjectColour.cs | 65 ++++++++++++------- .../Difficulty/Skills/Colour.cs | 11 +++- 4 files changed, 72 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 32bc8429b8..14f95fbdd5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -12,10 +12,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return Math.Tanh(Math.E * -(val - center) / width); } - public static double EvaluateDifficultyOf(DifficultyHitObject current) + public static double EvaluateDifficultyOf(TaikoDifficultyHitObjectColour colour) { - TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; - TaikoDifficultyHitObjectColour colour = taikoCurrent.Colour; if (colour == null) return 0; double objectStrain = 1.85; @@ -29,9 +27,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators objectStrain *= sigmoid(colour.DeltaRunLength, 2, 2) * 0.5 + 0.5; } - objectStrain *= -sigmoid(colour.RepetitionInterval, 1, 8); // * 0.5 + 0.5; - // Console.WriteLine($"{current.StartTime},{colour.Delta},{colour.RepetitionInterval},{objectStrain}"); + objectStrain *= -sigmoid(colour.RepetitionInterval, 1, 8); return objectStrain; } + + public static double EvaluateDifficultyOf(DifficultyHitObject current) + { + return EvaluateDifficultyOf(((TaikoDifficultyHitObject)current).Colour); + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index ed93595e93..f35cf1d8b9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { @@ -29,10 +30,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public readonly TaikoDifficultyHitObjectRhythm Rhythm; /// - /// Colour data for this hit object. This is used by colour evaluator to calculate colour, but can be used - /// differently by other skills in the future. + /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used + /// by other skills in the future. + /// This need to be writeable by TaikoDifficultyHitObjectColour so that it can assign potentially reused instances /// - public readonly TaikoDifficultyHitObjectColour Colour; + public TaikoDifficultyHitObjectColour Colour; /// /// The hit type of this hit object. @@ -64,8 +66,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing ); } - // Find repetition interval for the final TaikoDifficultyHitObjectColour - ((TaikoDifficultyHitObject)difficultyHitObject.Last()).Colour?.FindRepetitionInterval(); + List colours = TaikoDifficultyHitObjectColour.CreateColoursFor(difficultyHitObject); + + // Pre-evaluate colours + for (int i = 0; i < colours.Count; i++) + { + colours[i].EvaluatedDifficulty = ColourEvaluator.EvaluateDifficultyOf(colours[i]); + } + return difficultyHitObject; } @@ -115,9 +123,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing NoteIndex = noteObjects.Count; noteObjects.Add(this); - - // Need to be done after NoteIndex is set. - Colour = TaikoDifficultyHitObjectColour.GetInstanceFor(this); } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs index 8304c0b126..74b8899c60 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs @@ -1,6 +1,8 @@ #nullable disable using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { @@ -11,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { private const int max_repetition_interval = 16; - private TaikoDifficultyHitObjectColour previous; + public TaikoDifficultyHitObjectColour Previous { get; private set; } /// /// True if the current colour is different from the previous colour. @@ -30,35 +32,54 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public int RepetitionInterval { get; private set; } + /// + /// Evaluated colour difficulty is cached here, as difficulty don't need to be calculated per-note. + /// + /// TODO: Consider having all evaluated difficulty cached in TaikoDifficultyHitObject instead, since we may be + /// reusing evaluator results in the future. + public double EvaluatedDifficulty; + public TaikoDifficultyHitObjectColour repeatedColour { get; private set; } /// /// Get the instance for the given hitObject. This is implemented /// as a static function instead of constructor to allow for reusing existing instances. - /// TODO: findRepetitionInterval needs to be called a final time after all hitObjects have been processed. /// - public static TaikoDifficultyHitObjectColour GetInstanceFor(TaikoDifficultyHitObject hitObject) + public static List CreateColoursFor(List hitObjects) { - TaikoDifficultyHitObject lastObject = hitObject.PreviousNote(0); - TaikoDifficultyHitObjectColour previous = lastObject?.Colour; - bool delta = lastObject == null || hitObject.HitType != lastObject.HitType; + List colours = new List(); - if (previous != null && delta == previous.Delta) + for (int i = 0; i < hitObjects.Count; i++) { - previous.DeltaRunLength += 1; - return previous; + TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)hitObjects[i]; + TaikoDifficultyHitObject lastObject = hitObject.PreviousNote(0); + TaikoDifficultyHitObjectColour previous = lastObject?.Colour; + bool delta = lastObject == null || hitObject.HitType != lastObject.HitType; + + if (previous != null && delta == previous.Delta) + { + previous.DeltaRunLength += 1; + hitObject.Colour = previous; + continue; + } + + TaikoDifficultyHitObjectColour colour = new TaikoDifficultyHitObjectColour() + { + Delta = delta, + DeltaRunLength = 1, + RepetitionInterval = max_repetition_interval + 1, + Previous = previous + }; + hitObject.Colour = colour; + colours.Add(colour); } - // Calculate RepetitionInterval for previous - previous?.FindRepetitionInterval(); - - return new TaikoDifficultyHitObjectColour() + for (int i = 0; i < colours.Count; i++) { - Delta = delta, - DeltaRunLength = 1, - RepetitionInterval = max_repetition_interval + 1, - previous = previous - }; + colours[i].FindRepetitionInterval(); + } + + return colours; } /// @@ -67,14 +88,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public void FindRepetitionInterval() { - if (previous?.previous == null) + if (Previous?.Previous == null) { RepetitionInterval = max_repetition_interval + 1; return; } - int interval = previous.DeltaRunLength; - TaikoDifficultyHitObjectColour other = previous.previous; + int interval = Previous.DeltaRunLength; + TaikoDifficultyHitObjectColour other = Previous.Previous; while (other != null && interval < max_repetition_interval) { @@ -86,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } interval += other.DeltaRunLength; - other = other.previous; + other = other.Previous; } RepetitionInterval = max_repetition_interval + 1; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index a4f3f460c7..0d69104a58 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -6,7 +6,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { @@ -25,7 +25,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double StrainValueOf(DifficultyHitObject current) { - return ColourEvaluator.EvaluateDifficultyOf(current); + TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)current).Colour; + double difficulty = colour == null ? 0 : colour.EvaluatedDifficulty; + // if (current != null && colour != null) + // { + // System.Console.WriteLine($"{current.StartTime},{colour.Delta},{colour.RepetitionInterval},{difficulty}"); + // } + + return difficulty; } } } From 52de8bae9bfa6e1d6859e64335366bd445bbcf8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jun 2022 12:54:33 +0200 Subject: [PATCH 024/532] Apply NRT to `TaikoModClassic` --- osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs index 704185cc3c..9f82d54e65 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs @@ -1,10 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; @@ -21,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset, IUpdatableByPlayfield, IApplicableAfterBeatmapConversion { - private DrawableTaikoRuleset drawableTaikoRuleset; + private DrawableTaikoRuleset? drawableTaikoRuleset; public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { @@ -160,6 +159,8 @@ namespace osu.Game.Rulesets.Taiko.Mods // Classic taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened. const float scroll_rate = 10; + Debug.Assert(drawableTaikoRuleset != null); + // Since the time range will depend on a positional value, it is referenced to the x480 pixel space. float ratio = drawableTaikoRuleset.DrawHeight / 480; From bcb9cba2d7019473679e8eaf9c4c22dcbc61fa94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jun 2022 12:56:47 +0200 Subject: [PATCH 025/532] Reorder classes for legibility, group into regions --- .../Mods/TaikoModClassic.cs | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs index 9f82d54e65..f5541f5191 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs @@ -56,6 +56,13 @@ namespace osu.Game.Rulesets.Taiko.Mods taikoBeatmap.HitObjects = hitObjects; } + #region Classic drum roll + + private class TaikoClassicDrumRollJudgement : TaikoDrumRollJudgement + { + public override HitResult MaxResult => HitResult.IgnoreHit; + } + private class ClassicDrumRoll : DrumRoll { public ClassicDrumRoll(DrumRoll original) @@ -89,6 +96,11 @@ namespace osu.Game.Rulesets.Taiko.Mods } } + private class TaikoClassicDrumRollTickJudgement : TaikoDrumRollTickJudgement + { + public override HitResult MaxResult => HitResult.SmallBonus; + } + private class ClassicDrumRollTick : DrumRollTick { public override Judgement CreateJudgement() => new TaikoClassicDrumRollTickJudgement(); @@ -102,35 +114,6 @@ namespace osu.Game.Rulesets.Taiko.Mods } } - private class ClassicSwell : Swell - { - public ClassicSwell(Swell original) - { - StartTime = original.StartTime; - Samples = original.Samples; - EndTime = original.EndTime; - Duration = original.Duration; - RequiredHits = original.RequiredHits; - } - - public override Judgement CreateJudgement() => new TaikoClassicSwellJudgement(); - } - - private class TaikoClassicDrumRollJudgement : TaikoDrumRollJudgement - { - public override HitResult MaxResult => HitResult.IgnoreHit; - } - - private class TaikoClassicDrumRollTickJudgement : TaikoDrumRollTickJudgement - { - public override HitResult MaxResult => HitResult.SmallBonus; - } - - private class TaikoClassicSwellJudgement : TaikoSwellJudgement - { - public override HitResult MaxResult => HitResult.LargeBonus; - } - private class ClassicDrawableDrumRoll : DrawableDrumRoll { public override bool DisplayResult => false; @@ -147,6 +130,29 @@ namespace osu.Game.Rulesets.Taiko.Mods } } + #endregion + + #region Classic swell + + private class TaikoClassicSwellJudgement : TaikoSwellJudgement + { + public override HitResult MaxResult => HitResult.LargeBonus; + } + + private class ClassicSwell : Swell + { + public ClassicSwell(Swell original) + { + StartTime = original.StartTime; + Samples = original.Samples; + EndTime = original.EndTime; + Duration = original.Duration; + RequiredHits = original.RequiredHits; + } + + public override Judgement CreateJudgement() => new TaikoClassicSwellJudgement(); + } + private class ClassicDrawableSwell : DrawableSwell { public override bool DisplayResult => false; @@ -154,6 +160,8 @@ namespace osu.Game.Rulesets.Taiko.Mods protected override HitResult OkResult => HitResult.SmallBonus; } + #endregion + public void Update(Playfield playfield) { // Classic taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened. From f47b74a93882444db536c2764bd531a6a712d820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jun 2022 13:00:12 +0200 Subject: [PATCH 026/532] Move `OkResult` from drawable swell to judgement --- .../Judgements/TaikoSwellJudgement.cs | 5 +++++ osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs | 4 ++-- .../Objects/Drawables/DrawableSwell.cs | 9 ++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs index b2ac0b7f03..90ba7e276c 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs @@ -9,6 +9,11 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoSwellJudgement : TaikoJudgement { + /// + /// The to grant when the player has hit more than half of swell ticks. + /// + public virtual HitResult OkResult => HitResult.Ok; + protected override double HealthIncreaseFor(HitResult result) { switch (result) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs index f5541f5191..9100c0b4be 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs @@ -137,6 +137,8 @@ namespace osu.Game.Rulesets.Taiko.Mods private class TaikoClassicSwellJudgement : TaikoSwellJudgement { public override HitResult MaxResult => HitResult.LargeBonus; + + public override HitResult OkResult => HitResult.SmallBonus; } private class ClassicSwell : Swell @@ -156,8 +158,6 @@ namespace osu.Game.Rulesets.Taiko.Mods private class ClassicDrawableSwell : DrawableSwell { public override bool DisplayResult => false; - - protected override HitResult OkResult => HitResult.SmallBonus; } #endregion diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 5b70c59376..cb0f59a9fb 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -16,7 +16,7 @@ using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; @@ -34,7 +34,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// private const double ring_appear_offset = 100; - protected virtual HitResult OkResult => HitResult.Ok; private readonly Container ticks; private readonly Container bodyContainer; private readonly CircularContainer targetRing; @@ -223,7 +222,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables tick.TriggerResult(false); } - ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? OkResult : r.Judgement.MinResult); + ApplyResult(r => + { + var swellJudgement = (TaikoSwellJudgement)r.Judgement; + r.Type = numHits > HitObject.RequiredHits / 2 ? swellJudgement.OkResult : swellJudgement.MinResult; + }); } } From 3497e966fd97a834c17ad2046e59f37ba14f56c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jun 2022 13:01:16 +0200 Subject: [PATCH 027/532] Revert no longer needed access modifier change --- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 5e5d9daeb1..21c7f13fec 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Taiko.Objects /// The length (in milliseconds) between ticks of this drumroll. /// Half of this value is the hit window of the ticks. /// - protected double TickSpacing = 100; + private double tickSpacing = 100; private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY; @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Taiko.Objects double scoringDistance = base_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; - TickSpacing = timingPoint.BeatLength / TickRate; + tickSpacing = timingPoint.BeatLength / TickRate; overallDifficulty = difficulty.OverallDifficulty; } @@ -91,19 +91,19 @@ namespace osu.Game.Rulesets.Taiko.Objects { List ticks = new List(); - if (TickSpacing == 0) + if (tickSpacing == 0) return ticks; bool first = true; - for (double t = StartTime; t < EndTime + TickSpacing / 2; t += TickSpacing) + for (double t = StartTime; t < EndTime + tickSpacing / 2; t += tickSpacing) { cancellationToken.ThrowIfCancellationRequested(); ticks.Add(new DrumRollTick { FirstTick = first, - TickSpacing = TickSpacing, + TickSpacing = tickSpacing, StartTime = t, IsStrong = IsStrong }); From 09768cd74054e6cb806f0858bb5732bc9b27fd61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Jun 2022 13:43:02 +0200 Subject: [PATCH 028/532] Add test coverage --- .../Mods/TestSceneTaikoModClassic.cs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModClassic.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModClassic.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModClassic.cs new file mode 100644 index 0000000000..9028f411ce --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModClassic.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; + +namespace osu.Game.Rulesets.Taiko.Tests.Mods +{ + public class TestSceneTaikoModClassic : TaikoModTestScene + { + [Test] + public void TestHittingDrumRollsIsOptional() => CreateModTest(new ModTestData + { + Mod = new TaikoModClassic(), + Autoplay = false, + Beatmap = new TaikoBeatmap + { + BeatmapInfo = { Ruleset = CreatePlayerRuleset().RulesetInfo }, + HitObjects = new List + { + new Hit + { + StartTime = 1000, + Type = HitType.Centre + }, + new DrumRoll + { + StartTime = 3000, + EndTime = 6000 + } + } + }, + ReplayFrames = new List + { + new TaikoReplayFrame(1000, TaikoAction.LeftCentre), + new TaikoReplayFrame(1001) + }, + PassCondition = () => Player.ScoreProcessor.HasCompleted.Value + && Player.ScoreProcessor.Combo.Value == 1 + && Player.ScoreProcessor.Accuracy.Value == 1 + }); + + [Test] + public void TestHittingSwellsIsOptional() => CreateModTest(new ModTestData + { + Mod = new TaikoModClassic(), + Autoplay = false, + Beatmap = new TaikoBeatmap + { + BeatmapInfo = { Ruleset = CreatePlayerRuleset().RulesetInfo }, + HitObjects = new List + { + new Hit + { + StartTime = 1000, + Type = HitType.Centre + }, + new Swell + { + StartTime = 3000, + EndTime = 6000 + } + } + }, + ReplayFrames = new List + { + new TaikoReplayFrame(1000, TaikoAction.LeftCentre), + new TaikoReplayFrame(1001) + }, + PassCondition = () => Player.ScoreProcessor.HasCompleted.Value + && Player.ScoreProcessor.Combo.Value == 1 + && Player.ScoreProcessor.Accuracy.Value == 1 + }); + } +} From 15372267e17c78c919c764362d0d32c5c98d99cd Mon Sep 17 00:00:00 2001 From: vun Date: Sat, 25 Jun 2022 22:42:56 +0800 Subject: [PATCH 029/532] Implement new colour encoding --- .../Difficulty/Evaluators/ColourEvaluator.cs | 16 +--- .../Preprocessing/ColourEncoding.cs | 68 ++++++++++++++++ .../Preprocessing/CoupledColourEncoding.cs | 73 +++++++++++++++++ .../Preprocessing/TaikoDifficultyHitObject.cs | 12 +-- .../TaikoDifficultyHitObjectColour.cs | 81 +++++++------------ .../Difficulty/Skills/Colour.cs | 15 +++- 6 files changed, 188 insertions(+), 77 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/ColourEncoding.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/CoupledColourEncoding.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 14f95fbdd5..1627833e8a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -14,21 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators public static double EvaluateDifficultyOf(TaikoDifficultyHitObjectColour colour) { - if (colour == null) return 0; - - double objectStrain = 1.85; - - if (colour.Delta) - { - objectStrain *= sigmoid(colour.DeltaRunLength, 3, 3) * 0.5 + 0.5; - } - else - { - objectStrain *= sigmoid(colour.DeltaRunLength, 2, 2) * 0.5 + 0.5; - } - - objectStrain *= -sigmoid(colour.RepetitionInterval, 1, 8); - return objectStrain; + return 1; } public static double EvaluateDifficultyOf(DifficultyHitObject current) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/ColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/ColourEncoding.cs new file mode 100644 index 0000000000..e2ac40170e --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/ColourEncoding.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing +{ + public class ColourEncoding + { + /// + /// Amount consecutive notes of the same colour + /// + public int MonoRunLength = 1; + + /// + /// Amount of consecutive encoding with the same + /// + public int EncodingRunLength = 1; + + /// + /// Beginning index in the data that this encodes + /// + public int StartIndex = 0; + + public bool isIdenticalTo(ColourEncoding other) + { + return other.MonoRunLength == MonoRunLength && other.EncodingRunLength == EncodingRunLength; + } + + public static List Encode(List data) + { + // Encoding mono lengths + List firstPass = new List(); + ColourEncoding? lastEncoded = null; + for (int i = 0; i < data.Count; i++) + { + TaikoDifficultyHitObject taikoObject = (TaikoDifficultyHitObject)data[i]; + // This ignores all non-note objects, which may or may not be the desired behaviour + TaikoDifficultyHitObject previousObject = (TaikoDifficultyHitObject)taikoObject.PreviousNote(0); + + if (previousObject == null || lastEncoded == null || taikoObject.HitType != previousObject.HitType) + { + lastEncoded = new ColourEncoding(); + lastEncoded.StartIndex = i; + firstPass.Add(lastEncoded); + continue; + } + + lastEncoded.MonoRunLength += 1; + } + + // Encode encoding lengths + List secondPass = new List(); + lastEncoded = null; + for (int i = 0; i < firstPass.Count; i++) + { + if (i == 0 || lastEncoded == null || firstPass[i].MonoRunLength != firstPass[i - 1].MonoRunLength) + { + lastEncoded = firstPass[i]; + secondPass.Add(firstPass[i]); + continue; + } + + lastEncoded.EncodingRunLength += 1; + } + + return secondPass; + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/CoupledColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/CoupledColourEncoding.cs new file mode 100644 index 0000000000..81e8ae006f --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/CoupledColourEncoding.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing +{ + public class CoupledColourEncoding + { + public int RunLength = 1; + + public ColourEncoding[] Payload; + + /// + /// Beginning index in the data that this encodes + /// + public int StartIndex { get; private set; } = 0; + + public int EndIndex { get; private set; } = 0; + + private CoupledColourEncoding(ColourEncoding[] payload) + { + Payload = payload; + } + + public static List Encode(List data) + { + List encoded = new List(); + + CoupledColourEncoding? lastEncoded = null; + for (int i = 0; i < data.Count; i++) + { + if (lastEncoded != null) lastEncoded.EndIndex = data[i].StartIndex - 1; + + if (i >= data.Count - 2 || !data[i].isIdenticalTo(data[i + 2])) + { + lastEncoded = new CoupledColourEncoding(new ColourEncoding[] { data[i] }); + lastEncoded.StartIndex = data[i].StartIndex; + } + else + { + lastEncoded = new CoupledColourEncoding(new ColourEncoding[] { data[i], data[i + 1] }); + lastEncoded.StartIndex = data[i].StartIndex; + lastEncoded.RunLength = 3; + i++; + + // Peek 2 indices ahead + while (i < data.Count - 2 && data[i].isIdenticalTo(data[i + 2])) + { + lastEncoded.RunLength += 1; + i++; + } + + // Skip over peeked data + i++; + } + + encoded.Add(lastEncoded); + } + + return encoded; + } + + public bool hasIdenticalPayload(CoupledColourEncoding other) + { + if (this.Payload.Length != other.Payload.Length) return false; + + for (int i = 0; i < this.Payload.Length; i++) + { + if (!this.Payload[i].isIdenticalTo(other.Payload[i])) return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index f35cf1d8b9..1ee905d94c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -52,21 +52,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// The rate at which the gameplay clock is run at. public static List Create(IBeatmap beatmap, double clockRate) { - List difficultyHitObject = new List(); + List difficultyHitObjects = new List(); List centreObjects = new List(); List rimObjects = new List(); List noteObjects = new List(); for (int i = 2; i < beatmap.HitObjects.Count; i++) { - difficultyHitObject.Add( + difficultyHitObjects.Add( new TaikoDifficultyHitObject( - beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObject, - centreObjects, rimObjects, noteObjects, difficultyHitObject.Count) + beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects, + centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count) ); } - List colours = TaikoDifficultyHitObjectColour.CreateColoursFor(difficultyHitObject); + List colours = TaikoDifficultyHitObjectColour.EncodeAndAssign(difficultyHitObjects); // Pre-evaluate colours for (int i = 0; i < colours.Count; i++) @@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing colours[i].EvaluatedDifficulty = ColourEvaluator.EvaluateDifficultyOf(colours[i]); } - return difficultyHitObject; + return difficultyHitObjects; } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs index 74b8899c60..6bd99550be 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; @@ -11,72 +9,51 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public class TaikoDifficultyHitObjectColour { + public CoupledColourEncoding Encoding { get; private set; } + private const int max_repetition_interval = 16; - public TaikoDifficultyHitObjectColour Previous { get; private set; } - - /// - /// True if the current colour is different from the previous colour. - /// - public bool Delta { get; private set; } - - /// - /// How many notes are Delta repeated - /// - public int DeltaRunLength { get; private set; } - /// /// How many notes between the current and previous identical . /// Negative number means that there is no repetition in range. /// If no repetition is found this will have a value of + 1. /// - public int RepetitionInterval { get; private set; } + public int RepetitionInterval { get; private set; } = max_repetition_interval + 1; /// /// Evaluated colour difficulty is cached here, as difficulty don't need to be calculated per-note. /// /// TODO: Consider having all evaluated difficulty cached in TaikoDifficultyHitObject instead, since we may be /// reusing evaluator results in the future. - public double EvaluatedDifficulty; + public double EvaluatedDifficulty = 0; - public TaikoDifficultyHitObjectColour repeatedColour { get; private set; } + public TaikoDifficultyHitObjectColour? Previous { get; private set; } = null; - /// - /// Get the instance for the given hitObject. This is implemented - /// as a static function instead of constructor to allow for reusing existing instances. - /// - public static List CreateColoursFor(List hitObjects) + public TaikoDifficultyHitObjectColour? repeatedColour { get; private set; } = null; + + public TaikoDifficultyHitObjectColour(CoupledColourEncoding encoding) + { + Encoding = encoding; + } + + public static List EncodeAndAssign(List hitObjects) { List colours = new List(); - - for (int i = 0; i < hitObjects.Count; i++) + List encodings = CoupledColourEncoding.Encode(ColourEncoding.Encode(hitObjects)); + TaikoDifficultyHitObjectColour? lastColour = null; + for (int i = 0; i < encodings.Count; i++) { - TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)hitObjects[i]; - TaikoDifficultyHitObject lastObject = hitObject.PreviousNote(0); - TaikoDifficultyHitObjectColour previous = lastObject?.Colour; - bool delta = lastObject == null || hitObject.HitType != lastObject.HitType; - - if (previous != null && delta == previous.Delta) + lastColour = new TaikoDifficultyHitObjectColour(encodings[i]) { - previous.DeltaRunLength += 1; - hitObject.Colour = previous; - continue; - } - - TaikoDifficultyHitObjectColour colour = new TaikoDifficultyHitObjectColour() - { - Delta = delta, - DeltaRunLength = 1, - RepetitionInterval = max_repetition_interval + 1, - Previous = previous + Previous = lastColour }; - hitObject.Colour = colour; - colours.Add(colour); + colours.Add(lastColour); } - for (int i = 0; i < colours.Count; i++) + foreach (TaikoDifficultyHitObjectColour colour in colours) { - colours[i].FindRepetitionInterval(); + colour.FindRepetitionInterval(); + ((TaikoDifficultyHitObject)hitObjects[colour.Encoding.StartIndex]).Colour = colour; } return colours; @@ -94,23 +71,23 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing return; } - int interval = Previous.DeltaRunLength; - TaikoDifficultyHitObjectColour other = Previous.Previous; - - while (other != null && interval < max_repetition_interval) + TaikoDifficultyHitObjectColour? other = Previous.Previous; + int interval = this.Encoding.StartIndex - other.Encoding.EndIndex; + while (interval < max_repetition_interval) { - if (other.Delta == Delta && other.DeltaRunLength == DeltaRunLength) + if (Encoding.hasIdenticalPayload(other.Encoding)) { RepetitionInterval = Math.Min(interval, max_repetition_interval); repeatedColour = other; return; } - interval += other.DeltaRunLength; other = other.Previous; + if (other == null) break; + interval = this.Encoding.StartIndex - other.Encoding.EndIndex; } RepetitionInterval = max_repetition_interval + 1; } } -} +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 0d69104a58..13d5cd673e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -27,10 +27,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)current).Colour; double difficulty = colour == null ? 0 : colour.EvaluatedDifficulty; - // if (current != null && colour != null) - // { - // System.Console.WriteLine($"{current.StartTime},{colour.Delta},{colour.RepetitionInterval},{difficulty}"); - // } + if (current != null && colour != null) + { + ColourEncoding[] payload = colour.Encoding.Payload; + string payloadDisplay = ""; + for (int i = 0; i < payload.Length; ++i) + { + payloadDisplay += $",({payload[i].MonoRunLength},{payload[i].EncodingRunLength})"; + } + + System.Console.WriteLine($"{current.StartTime},{colour.RepetitionInterval},{colour.Encoding.RunLength}{payloadDisplay}"); + } return difficulty; } From 8c162585b8ebcf37f50009d17e44cff55d732784 Mon Sep 17 00:00:00 2001 From: vun Date: Sat, 25 Jun 2022 22:49:19 +0800 Subject: [PATCH 030/532] Comment out logging for debugging purposes --- .../Difficulty/Skills/Colour.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 13d5cd673e..bf359f0d64 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -27,17 +27,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)current).Colour; double difficulty = colour == null ? 0 : colour.EvaluatedDifficulty; - if (current != null && colour != null) - { - ColourEncoding[] payload = colour.Encoding.Payload; - string payloadDisplay = ""; - for (int i = 0; i < payload.Length; ++i) - { - payloadDisplay += $",({payload[i].MonoRunLength},{payload[i].EncodingRunLength})"; - } + // if (current != null && colour != null) + // { + // ColourEncoding[] payload = colour.Encoding.Payload; + // string payloadDisplay = ""; + // for (int i = 0; i < payload.Length; ++i) + // { + // payloadDisplay += $",({payload[i].MonoRunLength},{payload[i].EncodingRunLength})"; + // } - System.Console.WriteLine($"{current.StartTime},{colour.RepetitionInterval},{colour.Encoding.RunLength}{payloadDisplay}"); - } + // System.Console.WriteLine($"{current.StartTime},{colour.RepetitionInterval},{colour.Encoding.RunLength}{payloadDisplay}"); + // } return difficulty; } From cba47f82020b6c283e3b01c341c41949d5fb88da Mon Sep 17 00:00:00 2001 From: vun Date: Tue, 28 Jun 2022 10:38:58 +0800 Subject: [PATCH 031/532] [WIP] Colour evaluator for new colour encoding --- .../Difficulty/Evaluators/ColourEvaluator.cs | 14 +++++++++-- .../Difficulty/Evaluators/StaminaEvaluator.cs | 5 ++-- .../Preprocessing/ColourEncoding.cs | 17 ++++++++++--- .../Preprocessing/TaikoDifficultyHitObject.cs | 15 +++-------- .../TaikoDifficultyHitObjectColour.cs | 19 ++++++-------- .../Difficulty/Skills/Colour.cs | 25 +++++++++++++------ .../Difficulty/Skills/Peaks.cs | 9 +++---- 7 files changed, 61 insertions(+), 43 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 1627833e8a..60898fe92d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -12,9 +12,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return Math.Tanh(Math.E * -(val - center) / width); } - public static double EvaluateDifficultyOf(TaikoDifficultyHitObjectColour colour) + public static double EvaluateDifficultyOf(TaikoDifficultyHitObjectColour? colour) { - return 1; + if (colour == null) return 0; + + double difficulty = 7.5 * Math.Log(colour.Encoding.Payload.Length + 1, 10); + // foreach (ColourEncoding encoding in colour.Encoding.Payload) + // { + // difficulty += sigmoid(encoding.MonoRunLength, 1, 1) * 0.4 + 0.6; + // } + difficulty *= -sigmoid(colour.RepetitionInterval, 1, 7); + // difficulty *= -sigmoid(colour.RepetitionInterval, 2, 2) * 0.5 + 0.5; + + return difficulty; } public static double EvaluateDifficultyOf(DifficultyHitObject current) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index be1514891c..9ebdc90eb8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -16,7 +16,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// The interval between the current and previous note hit using the same key. private static double speedBonus(double interval) { - return Math.Pow(0.8, interval / 1000); + // return 10 / Math.Pow(interval, 0.6); + return Math.Pow(0.1, interval / 1000); } /// @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return 0.0; } - double objectStrain = 0.85; + double objectStrain = 1; objectStrain *= speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime); return objectStrain; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/ColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/ColourEncoding.cs index e2ac40170e..c090e7aada 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/ColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/ColourEncoding.cs @@ -14,7 +14,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// Amount of consecutive encoding with the same /// public int EncodingRunLength = 1; - + + /// + /// How many notes are encoded with this encoding + /// + public int NoteLength => MonoRunLength + EncodingRunLength; + /// /// Beginning index in the data that this encodes /// @@ -27,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public static List Encode(List data) { - // Encoding mono lengths + // Compute mono lengths List firstPass = new List(); ColourEncoding? lastEncoded = null; for (int i = 0; i < data.Count; i++) @@ -36,7 +41,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing // This ignores all non-note objects, which may or may not be the desired behaviour TaikoDifficultyHitObject previousObject = (TaikoDifficultyHitObject)taikoObject.PreviousNote(0); - if (previousObject == null || lastEncoded == null || taikoObject.HitType != previousObject.HitType) + if ( + previousObject == null || + lastEncoded == null || + taikoObject.HitType != previousObject.HitType || + taikoObject.Rhythm.Ratio > 1.9) // Reset colour after a slow down of 2x (set as 1.9x for margin of error) { lastEncoded = new ColourEncoding(); lastEncoded.StartIndex = i; @@ -47,7 +56,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing lastEncoded.MonoRunLength += 1; } - // Encode encoding lengths + // Compute encoding lengths List secondPass = new List(); lastEncoded = null; for (int i = 0; i < firstPass.Count; i++) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 1ee905d94c..7c9188b100 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -19,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public class TaikoDifficultyHitObject : DifficultyHitObject { - private readonly IReadOnlyList monoDifficultyHitObjects; + private readonly IReadOnlyList? monoDifficultyHitObjects; public readonly int MonoIndex; private readonly IReadOnlyList noteObjects; public readonly int NoteIndex; @@ -34,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// by other skills in the future. /// This need to be writeable by TaikoDifficultyHitObjectColour so that it can assign potentially reused instances /// - public TaikoDifficultyHitObjectColour Colour; + public TaikoDifficultyHitObjectColour? Colour; /// /// The hit type of this hit object. @@ -65,14 +63,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count) ); } - - List colours = TaikoDifficultyHitObjectColour.EncodeAndAssign(difficultyHitObjects); - - // Pre-evaluate colours - for (int i = 0; i < colours.Count; i++) - { - colours[i].EvaluatedDifficulty = ColourEvaluator.EvaluateDifficultyOf(colours[i]); - } + TaikoDifficultyHitObjectColour.EncodeAndAssign(difficultyHitObjects); return difficultyHitObjects; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs index 6bd99550be..7e18332fab 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs @@ -5,12 +5,11 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { /// - /// Stores colour compression information for a . + /// Stores colour compression information for a . This is only present for the + /// first in a chunk. /// public class TaikoDifficultyHitObjectColour { - public CoupledColourEncoding Encoding { get; private set; } - private const int max_repetition_interval = 16; /// @@ -21,11 +20,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public int RepetitionInterval { get; private set; } = max_repetition_interval + 1; /// - /// Evaluated colour difficulty is cached here, as difficulty don't need to be calculated per-note. + /// Encoding information of . /// - /// TODO: Consider having all evaluated difficulty cached in TaikoDifficultyHitObject instead, since we may be - /// reusing evaluator results in the future. - public double EvaluatedDifficulty = 0; + public CoupledColourEncoding Encoding { get; private set; } public TaikoDifficultyHitObjectColour? Previous { get; private set; } = null; @@ -60,8 +57,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } /// - /// Finds the closest previous that has the identical delta value - /// and run length with the current instance, and returns the amount of notes between them. + /// Finds the closest previous that has the identical . + /// Interval is defined as the amount of chunks between the current and repeated encoding. /// public void FindRepetitionInterval() { @@ -72,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } TaikoDifficultyHitObjectColour? other = Previous.Previous; - int interval = this.Encoding.StartIndex - other.Encoding.EndIndex; + int interval = 2; while (interval < max_repetition_interval) { if (Encoding.hasIdenticalPayload(other.Encoding)) @@ -84,7 +81,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing other = other.Previous; if (other == null) break; - interval = this.Encoding.StartIndex - other.Encoding.EndIndex; + ++interval; } RepetitionInterval = max_repetition_interval + 1; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index bf359f0d64..c0dafc73b5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -1,12 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { @@ -18,6 +18,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; + /// + /// Applies a speed bonus dependent on the time since the last hit. + /// + /// The interval between the current and previous note hit using the same key. + private static double speedBonus(double interval) + { + return Math.Pow(0.4, interval / 1000); + } + public Colour(Mod[] mods) : base(mods) { @@ -25,18 +34,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double StrainValueOf(DifficultyHitObject current) { - TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)current).Colour; - double difficulty = colour == null ? 0 : colour.EvaluatedDifficulty; - // if (current != null && colour != null) + double difficulty = ColourEvaluator.EvaluateDifficultyOf(current); + // difficulty *= speedBonus(current.DeltaTime); + // TaikoDifficultyHitObject? taikoCurrent = (TaikoDifficultyHitObject)current; + // TaikoDifficultyHitObjectColour? colour = taikoCurrent?.Colour; + // if (taikoCurrent != null && colour != null) // { // ColourEncoding[] payload = colour.Encoding.Payload; // string payloadDisplay = ""; // for (int i = 0; i < payload.Length; ++i) // { - // payloadDisplay += $",({payload[i].MonoRunLength},{payload[i].EncodingRunLength})"; + // payloadDisplay += $",({payload[i].MonoRunLength}|{payload[i].EncodingRunLength})"; // } - // System.Console.WriteLine($"{current.StartTime},{colour.RepetitionInterval},{colour.Encoding.RunLength}{payloadDisplay}"); + // System.Console.WriteLine($"{current.StartTime},{difficulty},{colour.RepetitionInterval},{colour.Encoding.RunLength}{payloadDisplay}"); // } return difficulty; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs index ebbe027f9e..7b6fb7d102 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs @@ -4,17 +4,16 @@ using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Peaks : Skill { - private const double rhythm_skill_multiplier = 0.32 * final_multiplier; - private const double colour_skill_multiplier = 0.33 * final_multiplier; - private const double stamina_skill_multiplier = 0.4 * final_multiplier; + private const double rhythm_skill_multiplier = 0.3 * final_multiplier; + private const double colour_skill_multiplier = 0.39 * final_multiplier; + private const double stamina_skill_multiplier = 0.33 * final_multiplier; - private const double final_multiplier = 0.047; + private const double final_multiplier = 0.06; private readonly Rhythm rhythm; private readonly Colour colour; From 2b42dfb9cbc81f2d567f56bda28f4ebf2bcaf199 Mon Sep 17 00:00:00 2001 From: 63411 <62799417+molneya@users.noreply.github.com> Date: Tue, 28 Jun 2022 20:32:35 +0800 Subject: [PATCH 032/532] apply individualStrain decay only when a note appears in that column --- .../Difficulty/Skills/Strain.cs | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index c2e532430c..74334c90e4 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -21,7 +21,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 1; - private readonly double[] holdEndTimes; + private readonly double[] startTimes; + private readonly double[] endTimes; private readonly double[] individualStrains; private double individualStrain; @@ -30,7 +31,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills public Strain(Mod[] mods, int totalColumns) : base(mods) { - holdEndTimes = new double[totalColumns]; + startTimes = new double[totalColumns]; + endTimes = new double[totalColumns]; individualStrains = new double[totalColumns]; overallStrain = 1; } @@ -38,32 +40,27 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills protected override double StrainValueOf(DifficultyHitObject current) { var maniaCurrent = (ManiaDifficultyHitObject)current; + double startTime = maniaCurrent.StartTime; double endTime = maniaCurrent.EndTime; int column = maniaCurrent.BaseObject.Column; - double closestEndTime = Math.Abs(endTime - maniaCurrent.LastObject.StartTime); // Lowest value we can assume with the current information - - double holdFactor = 1.0; // Factor to all additional strains in case something else is held - double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly bool isOverlapping = false; - // Fill up the holdEndTimes array - for (int i = 0; i < holdEndTimes.Length; ++i) + double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information + double holdFactor = 1.0; // Factor to all additional strains in case something else is held + double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly + + for (int i = 0; i < endTimes.Length; ++i) { // The current note is overlapped if a previous note or end is overlapping the current note body - isOverlapping |= Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1); + isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) && Precision.DefinitelyBigger(endTime, endTimes[i], 1); // We give a slight bonus to everything if something is held meanwhile - if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1)) + if (Precision.DefinitelyBigger(endTimes[i], endTime, 1)) holdFactor = 1.25; - closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - holdEndTimes[i])); - - // Decay individual strains - individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base); + closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i])); } - holdEndTimes[column] = endTime; - // The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending. // Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away. // holdAddition @@ -77,11 +74,19 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills if (isOverlapping) holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime))); - // Increase individual strain in own column + // Decay and increase individualStrains in own column + individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base); individualStrains[column] += 2.0 * holdFactor; + individualStrain = individualStrains[column]; - overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor; + // Decay and increase overallStrain + overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base); + overallStrain += (1 + holdAddition) * holdFactor; + + // Update startTimes and endTimes arrays + startTimes[column] = startTime; + endTimes[column] = endTime; return individualStrain + overallStrain - CurrentStrain; } From 5f8d21f33d60c422eb44d5736e6a66aad41290f2 Mon Sep 17 00:00:00 2001 From: vun Date: Fri, 1 Jul 2022 14:27:23 +0800 Subject: [PATCH 033/532] Per encoding evaluation --- .../Difficulty/Evaluators/ColourEvaluator.cs | 22 +++++++++-- .../Difficulty/Skills/Colour.cs | 39 ++++++++++++------- .../Difficulty/Skills/Peaks.cs | 27 +++++++++++-- .../Difficulty/TaikoDifficultyCalculator.cs | 2 +- 4 files changed, 68 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 60898fe92d..8d9f50eee5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -12,17 +12,33 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return Math.Tanh(Math.E * -(val - center) / width); } + public static double EvaluateDifficultyOf(ColourEncoding encoding) + { + return 1 / Math.Pow(encoding.MonoRunLength, 0.5); + } + + public static double EvaluateDifficultyOf(CoupledColourEncoding coupledEncoding) + { + double difficulty = 0; + for (int i = 0; i < coupledEncoding.Payload.Length; i++) + { + difficulty += EvaluateDifficultyOf(coupledEncoding.Payload[i]); + } + return difficulty; + } + public static double EvaluateDifficultyOf(TaikoDifficultyHitObjectColour? colour) { if (colour == null) return 0; - double difficulty = 7.5 * Math.Log(colour.Encoding.Payload.Length + 1, 10); + // double difficulty = 9.5 * Math.Log(colour.Encoding.Payload.Length + 1, 10); + double difficulty = 3 * EvaluateDifficultyOf(colour.Encoding); // foreach (ColourEncoding encoding in colour.Encoding.Payload) // { // difficulty += sigmoid(encoding.MonoRunLength, 1, 1) * 0.4 + 0.6; // } - difficulty *= -sigmoid(colour.RepetitionInterval, 1, 7); - // difficulty *= -sigmoid(colour.RepetitionInterval, 2, 2) * 0.5 + 0.5; + // difficulty *= -sigmoid(colour.RepetitionInterval, 1, 7); + difficulty *= -sigmoid(colour.RepetitionInterval, 6, 5) * 0.5 + 0.5; return difficulty; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index c0dafc73b5..6b7138fa92 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -35,22 +35,31 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double StrainValueOf(DifficultyHitObject current) { double difficulty = ColourEvaluator.EvaluateDifficultyOf(current); - // difficulty *= speedBonus(current.DeltaTime); - // TaikoDifficultyHitObject? taikoCurrent = (TaikoDifficultyHitObject)current; - // TaikoDifficultyHitObjectColour? colour = taikoCurrent?.Colour; - // if (taikoCurrent != null && colour != null) - // { - // ColourEncoding[] payload = colour.Encoding.Payload; - // string payloadDisplay = ""; - // for (int i = 0; i < payload.Length; ++i) - // { - // payloadDisplay += $",({payload[i].MonoRunLength}|{payload[i].EncodingRunLength})"; - // } - - // System.Console.WriteLine($"{current.StartTime},{difficulty},{colour.RepetitionInterval},{colour.Encoding.RunLength}{payloadDisplay}"); - // } - return difficulty; } + + // TODO: Remove befor pr + public string GetDebugString(DifficultyHitObject current) + { + double difficulty = ColourEvaluator.EvaluateDifficultyOf(current); + difficulty *= speedBonus(current.DeltaTime); + TaikoDifficultyHitObject? taikoCurrent = (TaikoDifficultyHitObject)current; + TaikoDifficultyHitObjectColour? colour = taikoCurrent?.Colour; + if (taikoCurrent != null && colour != null) + { + ColourEncoding[] payload = colour.Encoding.Payload; + string payloadDisplay = ""; + for (int i = 0; i < payload.Length; ++i) + { + payloadDisplay += $"({payload[i].MonoRunLength}|{payload[i].EncodingRunLength})"; + } + + return $"{current.StartTime},{difficulty},{CurrentStrain},{colour.RepetitionInterval},{colour.Encoding.RunLength},{payloadDisplay}"; + } + else + { + return $"{current.StartTime},{difficulty},{CurrentStrain},0,0,0"; + } + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs index 7b6fb7d102..1086cf5f72 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs @@ -1,17 +1,19 @@ using System; +using System.IO; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; +using osu.Game.Beatmaps; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Peaks : Skill { private const double rhythm_skill_multiplier = 0.3 * final_multiplier; - private const double colour_skill_multiplier = 0.39 * final_multiplier; - private const double stamina_skill_multiplier = 0.33 * final_multiplier; + private const double colour_skill_multiplier = 0.375 * final_multiplier; + private const double stamina_skill_multiplier = 0.375 * final_multiplier; private const double final_multiplier = 0.06; @@ -23,12 +25,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public double RhythmDifficultyValue => rhythm.DifficultyValue() * rhythm_skill_multiplier; public double StaminaDifficultyValue => stamina.DifficultyValue() * stamina_skill_multiplier; - public Peaks(Mod[] mods) + // TODO: remove before pr + private StreamWriter? colourDebugOutput; + bool debugColour = false; + + public Peaks(Mod[] mods, IBeatmap beatmap) : base(mods) { rhythm = new Rhythm(mods); colour = new Colour(mods); stamina = new Stamina(mods); + + if (debugColour) + { + String filename = $"{beatmap.BeatmapInfo.Metadata.Title}[{beatmap.BeatmapInfo.DifficultyName}].csv"; + filename = filename.Replace('/', '_'); + colourDebugOutput = new StreamWriter(File.OpenWrite($"/run/mount/secondary/workspace/osu/output/colour-debug/{filename}")); + colourDebugOutput.WriteLine("StartTime,Raw,Decayed,RepetitionInterval,EncodingRunLength,Payload"); + } + } /// @@ -43,6 +58,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills rhythm.Process(current); colour.Process(current); stamina.Process(current); + + if (debugColour && colourDebugOutput != null) + { + colourDebugOutput.WriteLine(colour.GetDebugString(current)); + colourDebugOutput.Flush(); + } } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index ace3599f28..2982861e0b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { return new Skill[] { - new Peaks(mods) + new Peaks(mods, beatmap) }; } From 505a24a68ef4239df119aec3227f8a9afa15be95 Mon Sep 17 00:00:00 2001 From: vun Date: Tue, 5 Jul 2022 14:41:40 +0800 Subject: [PATCH 034/532] Implement new colour encoding and evaluator --- .../Difficulty/Evaluators/ColourEvaluator.cs | 79 ++++++++---- .../Preprocessing/Colour/ColourEncoding.cs | 42 ++++++ .../Colour/CoupledColourEncoding.cs | 122 ++++++++++++++++++ .../Preprocessing/Colour/MonoEncoding.cs | 40 ++++++ .../Colour/TaikoDifficultyHitObjectColour.cs | 50 +++++++ .../Preprocessing/ColourEncoding.cs | 77 ----------- .../Preprocessing/CoupledColourEncoding.cs | 73 ----------- .../Preprocessing/TaikoDifficultyHitObject.cs | 3 +- .../TaikoDifficultyHitObjectColour.cs | 90 ------------- .../Difficulty/Skills/Colour.cs | 19 ++- .../Difficulty/Skills/Peaks.cs | 4 +- 11 files changed, 326 insertions(+), 273 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/MonoEncoding.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/ColourEncoding.cs delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/CoupledColourEncoding.cs delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 8d9f50eee5..d33f2a85b9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -1,6 +1,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { @@ -12,40 +13,70 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return Math.Tanh(Math.E * -(val - center) / width); } - public static double EvaluateDifficultyOf(ColourEncoding encoding) + private static double sigmoid(double val, double center, double width, double middle, double height) { - return 1 / Math.Pow(encoding.MonoRunLength, 0.5); + return sigmoid(val, center, width) * (height / 2) + middle; } - public static double EvaluateDifficultyOf(CoupledColourEncoding coupledEncoding) + /// + /// Evaluate the difficulty of the first note of a . + /// The encoding to evaluate. + /// The index of the mono encoding within it's parent . + /// + public static double EvaluateDifficultyOf(MonoEncoding encoding, int i) { - double difficulty = 0; - for (int i = 0; i < coupledEncoding.Payload.Length; i++) + return sigmoid(i, 2, 2, 0.5, 1); + } + + /// + /// Evaluate the difficulty of the first note of a . + /// + /// The encoding to evaluate. + /// The index of the colour encoding within it's parent . + public static double EvaluateDifficultyOf(ColourEncoding encoding, int i) + { + return sigmoid(i, 2, 2, 0.5, 1); + } + + /// + /// Evaluate the difficulty of the first note of a . + /// + public static double EvaluateDifficultyOf(CoupledColourEncoding encoding) + { + return 1 - sigmoid(encoding.RepetitionInterval, 2, 2, 0.5, 1); + } + + /// + /// Pre-evaluate and *assign* difficulty values of all hit objects encoded in a . + /// Difficulty values are assigned to of each + /// encoded within. + /// + public static void PreEvaluateDifficulties(CoupledColourEncoding encoding) + { + double coupledEncodingDifficulty = EvaluateDifficultyOf(encoding); + encoding.Payload[0].Payload[0].EncodedData[0].Colour!.EvaluatedDifficulty += coupledEncodingDifficulty; + for (int i = 0; i < encoding.Payload.Count; i++) { - difficulty += EvaluateDifficultyOf(coupledEncoding.Payload[i]); + ColourEncoding colourEncoding = encoding.Payload[i]; + double colourEncodingDifficulty = EvaluateDifficultyOf(colourEncoding, i) * coupledEncodingDifficulty; + colourEncoding.Payload[0].EncodedData[0].Colour!.EvaluatedDifficulty += colourEncodingDifficulty; + for (int j = 0; j < colourEncoding.Payload.Count; j++) + { + MonoEncoding monoEncoding = colourEncoding.Payload[j]; + monoEncoding.EncodedData[0].Colour!.EvaluatedDifficulty += EvaluateDifficultyOf(monoEncoding, j) * colourEncodingDifficulty; + } } - return difficulty; - } - - public static double EvaluateDifficultyOf(TaikoDifficultyHitObjectColour? colour) - { - if (colour == null) return 0; - - // double difficulty = 9.5 * Math.Log(colour.Encoding.Payload.Length + 1, 10); - double difficulty = 3 * EvaluateDifficultyOf(colour.Encoding); - // foreach (ColourEncoding encoding in colour.Encoding.Payload) - // { - // difficulty += sigmoid(encoding.MonoRunLength, 1, 1) * 0.4 + 0.6; - // } - // difficulty *= -sigmoid(colour.RepetitionInterval, 1, 7); - difficulty *= -sigmoid(colour.RepetitionInterval, 6, 5) * 0.5 + 0.5; - - return difficulty; } public static double EvaluateDifficultyOf(DifficultyHitObject current) { - return EvaluateDifficultyOf(((TaikoDifficultyHitObject)current).Colour); + TaikoDifficultyHitObject? taikoObject = current as TaikoDifficultyHitObject; + if (taikoObject != null && taikoObject.Colour != null) + { + return taikoObject.Colour.EvaluatedDifficulty; + } + + return 0; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs new file mode 100644 index 0000000000..bc427d87ae --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour +{ + public class ColourEncoding + { + public List Payload { get; private set; } = new List(); + + public bool isIdenticalTo(ColourEncoding other) + { + return other.Payload[0].RunLength == Payload[0].RunLength && + other.Payload[0].EncodedData[0].HitType == Payload[0].EncodedData[0].HitType; + } + + public bool hasIdenticalMonoLength(ColourEncoding other) + { + return other.Payload[0].RunLength == Payload[0].RunLength; + } + + public static List Encode(List data) + { + // Compute encoding lengths + List encoded = new List(); + ColourEncoding? lastEncoded = null; + for (int i = 0; i < data.Count; i++) + { + if (i == 0 || lastEncoded == null || data[i].RunLength != data[i - 1].RunLength) + { + lastEncoded = new ColourEncoding(); + lastEncoded.Payload.Add(data[i]); + encoded.Add(lastEncoded); + continue; + } + + lastEncoded.Payload.Add(data[i]); + } + + return encoded; + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs new file mode 100644 index 0000000000..7bdcf50055 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour +{ + public class CoupledColourEncoding + { + private const int max_repetition_interval = 16; + + public List Payload = new List(); + + public CoupledColourEncoding? Previous { get; private set; } = null; + + /// + /// How many notes between the current and previous identical . + /// Negative number means that there is no repetition in range. + /// If no repetition is found this will have a value of + 1. + /// + public int RepetitionInterval { get; private set; } = max_repetition_interval + 1; + + public static List Encode(List data) + { + List firstPass = MonoEncoding.Encode(data); + List secondPass = ColourEncoding.Encode(firstPass); + List thirdPass = CoupledColourEncoding.Encode(secondPass); + + return thirdPass; + } + + public static List Encode(List data) + { + List encoded = new List(); + + CoupledColourEncoding? lastEncoded = null; + for (int i = 0; i < data.Count; i++) + { + lastEncoded = new CoupledColourEncoding() + { + Previous = lastEncoded + }; + + bool isCoupled = i < data.Count - 2 && data[i].isIdenticalTo(data[i + 2]); + if (!isCoupled) + { + lastEncoded.Payload.Add(data[i]); + } + else + { + while (isCoupled) + { + lastEncoded.Payload.Add(data[i]); + i++; + + isCoupled = i < data.Count - 2 && data[i].isIdenticalTo(data[i + 2]); + } + + // Skip over peeked data and add the rest to the payload + lastEncoded.Payload.Add(data[i]); + lastEncoded.Payload.Add(data[i + 1]); + i++; + } + + encoded.Add(lastEncoded); + } + + // Final pass to find repetition interval + for (int i = 0; i < encoded.Count; i++) + { + encoded[i].FindRepetitionInterval(); + } + + return encoded; + } + + /// + /// Returns true if other is considered a repetition of this encoding. This is true if other's first two payload + /// identical mono lengths. + /// + public bool isRepetitionOf(CoupledColourEncoding other) + { + if (this.Payload.Count != other.Payload.Count) return false; + + for (int i = 0; i < Math.Min(this.Payload.Count, 2); i++) + { + if (!this.Payload[i].hasIdenticalMonoLength(other.Payload[i])) return false; + } + + return true; + } + + /// + /// Finds the closest previous that has the identical . + /// Interval is defined as the amount of chunks between the current and repeated encoding. + /// + public void FindRepetitionInterval() + { + if (Previous?.Previous == null) + { + RepetitionInterval = max_repetition_interval + 1; + return; + } + + CoupledColourEncoding? other = Previous.Previous; + int interval = 2; + while (interval < max_repetition_interval) + { + if (this.isRepetitionOf(other)) + { + RepetitionInterval = Math.Min(interval, max_repetition_interval); + return; + } + + other = other.Previous; + if (other == null) break; + ++interval; + } + + RepetitionInterval = max_repetition_interval + 1; + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/MonoEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/MonoEncoding.cs new file mode 100644 index 0000000000..92cdb0667b --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/MonoEncoding.cs @@ -0,0 +1,40 @@ +using osu.Game.Rulesets.Difficulty.Preprocessing; +using System.Collections.Generic; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour +{ + public class MonoEncoding + { + public List EncodedData { get; private set; } = new List(); + + public int RunLength => EncodedData.Count; + + public static List Encode(List data) + { + List encoded = new List(); + + MonoEncoding? lastEncoded = null; + for (int i = 0; i < data.Count; i++) + { + TaikoDifficultyHitObject taikoObject = (TaikoDifficultyHitObject)data[i]; + // This ignores all non-note objects, which may or may not be the desired behaviour + TaikoDifficultyHitObject previousObject = (TaikoDifficultyHitObject)taikoObject.PreviousNote(0); + + if ( + previousObject == null || + lastEncoded == null || + taikoObject.HitType != previousObject.HitType) + { + lastEncoded = new MonoEncoding(); + lastEncoded.EncodedData.Add(taikoObject); + encoded.Add(lastEncoded); + continue; + } + + lastEncoded.EncodedData.Add(taikoObject); + } + + return encoded; + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs new file mode 100644 index 0000000000..7dfd0fdbd4 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour +{ + /// + /// Stores colour compression information for a . This is only present for the + /// first in a chunk. + /// + public class TaikoDifficultyHitObjectColour + { + public CoupledColourEncoding Encoding { get; private set; } + + public double EvaluatedDifficulty = 0; + + private TaikoDifficultyHitObjectColour(CoupledColourEncoding encoding) + { + Encoding = encoding; + } + + // TODO: Might wanna move this somewhere else as it is introducing circular references + public static List EncodeAndAssign(List hitObjects) + { + List colours = new List(); + List encodings = CoupledColourEncoding.Encode(hitObjects); + + // Assign colour to objects + encodings.ForEach(coupledEncoding => + { + coupledEncoding.Payload.ForEach(encoding => + { + encoding.Payload.ForEach(mono => + { + mono.EncodedData.ForEach(hitObject => + { + hitObject.Colour = new TaikoDifficultyHitObjectColour(coupledEncoding); + }); + }); + }); + + // Preevaluate and assign difficulty values + ColourEvaluator.PreEvaluateDifficulties(coupledEncoding); + }); + + return colours; + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/ColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/ColourEncoding.cs deleted file mode 100644 index c090e7aada..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/ColourEncoding.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Collections.Generic; -using osu.Game.Rulesets.Difficulty.Preprocessing; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing -{ - public class ColourEncoding - { - /// - /// Amount consecutive notes of the same colour - /// - public int MonoRunLength = 1; - - /// - /// Amount of consecutive encoding with the same - /// - public int EncodingRunLength = 1; - - /// - /// How many notes are encoded with this encoding - /// - public int NoteLength => MonoRunLength + EncodingRunLength; - - /// - /// Beginning index in the data that this encodes - /// - public int StartIndex = 0; - - public bool isIdenticalTo(ColourEncoding other) - { - return other.MonoRunLength == MonoRunLength && other.EncodingRunLength == EncodingRunLength; - } - - public static List Encode(List data) - { - // Compute mono lengths - List firstPass = new List(); - ColourEncoding? lastEncoded = null; - for (int i = 0; i < data.Count; i++) - { - TaikoDifficultyHitObject taikoObject = (TaikoDifficultyHitObject)data[i]; - // This ignores all non-note objects, which may or may not be the desired behaviour - TaikoDifficultyHitObject previousObject = (TaikoDifficultyHitObject)taikoObject.PreviousNote(0); - - if ( - previousObject == null || - lastEncoded == null || - taikoObject.HitType != previousObject.HitType || - taikoObject.Rhythm.Ratio > 1.9) // Reset colour after a slow down of 2x (set as 1.9x for margin of error) - { - lastEncoded = new ColourEncoding(); - lastEncoded.StartIndex = i; - firstPass.Add(lastEncoded); - continue; - } - - lastEncoded.MonoRunLength += 1; - } - - // Compute encoding lengths - List secondPass = new List(); - lastEncoded = null; - for (int i = 0; i < firstPass.Count; i++) - { - if (i == 0 || lastEncoded == null || firstPass[i].MonoRunLength != firstPass[i - 1].MonoRunLength) - { - lastEncoded = firstPass[i]; - secondPass.Add(firstPass[i]); - continue; - } - - lastEncoded.EncodingRunLength += 1; - } - - return secondPass; - } - } -} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/CoupledColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/CoupledColourEncoding.cs deleted file mode 100644 index 81e8ae006f..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/CoupledColourEncoding.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Collections.Generic; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing -{ - public class CoupledColourEncoding - { - public int RunLength = 1; - - public ColourEncoding[] Payload; - - /// - /// Beginning index in the data that this encodes - /// - public int StartIndex { get; private set; } = 0; - - public int EndIndex { get; private set; } = 0; - - private CoupledColourEncoding(ColourEncoding[] payload) - { - Payload = payload; - } - - public static List Encode(List data) - { - List encoded = new List(); - - CoupledColourEncoding? lastEncoded = null; - for (int i = 0; i < data.Count; i++) - { - if (lastEncoded != null) lastEncoded.EndIndex = data[i].StartIndex - 1; - - if (i >= data.Count - 2 || !data[i].isIdenticalTo(data[i + 2])) - { - lastEncoded = new CoupledColourEncoding(new ColourEncoding[] { data[i] }); - lastEncoded.StartIndex = data[i].StartIndex; - } - else - { - lastEncoded = new CoupledColourEncoding(new ColourEncoding[] { data[i], data[i + 1] }); - lastEncoded.StartIndex = data[i].StartIndex; - lastEncoded.RunLength = 3; - i++; - - // Peek 2 indices ahead - while (i < data.Count - 2 && data[i].isIdenticalTo(data[i + 2])) - { - lastEncoded.RunLength += 1; - i++; - } - - // Skip over peeked data - i++; - } - - encoded.Add(lastEncoded); - } - - return encoded; - } - - public bool hasIdenticalPayload(CoupledColourEncoding other) - { - if (this.Payload.Length != other.Payload.Length) return false; - - for (int i = 0; i < this.Payload.Length; i++) - { - if (!this.Payload[i].isIdenticalTo(other.Payload[i])) return false; - } - - return true; - } - } -} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 7c9188b100..919770afc8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing @@ -63,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count) ); } - TaikoDifficultyHitObjectColour.EncodeAndAssign(difficultyHitObjects); + var encoded = TaikoDifficultyHitObjectColour.EncodeAndAssign(difficultyHitObjects); return difficultyHitObjects; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs deleted file mode 100644 index 7e18332fab..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectColour.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using osu.Game.Rulesets.Difficulty.Preprocessing; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing -{ - /// - /// Stores colour compression information for a . This is only present for the - /// first in a chunk. - /// - public class TaikoDifficultyHitObjectColour - { - private const int max_repetition_interval = 16; - - /// - /// How many notes between the current and previous identical . - /// Negative number means that there is no repetition in range. - /// If no repetition is found this will have a value of + 1. - /// - public int RepetitionInterval { get; private set; } = max_repetition_interval + 1; - - /// - /// Encoding information of . - /// - public CoupledColourEncoding Encoding { get; private set; } - - public TaikoDifficultyHitObjectColour? Previous { get; private set; } = null; - - public TaikoDifficultyHitObjectColour? repeatedColour { get; private set; } = null; - - public TaikoDifficultyHitObjectColour(CoupledColourEncoding encoding) - { - Encoding = encoding; - } - - public static List EncodeAndAssign(List hitObjects) - { - List colours = new List(); - List encodings = CoupledColourEncoding.Encode(ColourEncoding.Encode(hitObjects)); - TaikoDifficultyHitObjectColour? lastColour = null; - for (int i = 0; i < encodings.Count; i++) - { - lastColour = new TaikoDifficultyHitObjectColour(encodings[i]) - { - Previous = lastColour - }; - colours.Add(lastColour); - } - - foreach (TaikoDifficultyHitObjectColour colour in colours) - { - colour.FindRepetitionInterval(); - ((TaikoDifficultyHitObject)hitObjects[colour.Encoding.StartIndex]).Colour = colour; - } - - return colours; - } - - /// - /// Finds the closest previous that has the identical . - /// Interval is defined as the amount of chunks between the current and repeated encoding. - /// - public void FindRepetitionInterval() - { - if (Previous?.Previous == null) - { - RepetitionInterval = max_repetition_interval + 1; - return; - } - - TaikoDifficultyHitObjectColour? other = Previous.Previous; - int interval = 2; - while (interval < max_repetition_interval) - { - if (Encoding.hasIdenticalPayload(other.Encoding)) - { - RepetitionInterval = Math.Min(interval, max_repetition_interval); - repeatedColour = other; - return; - } - - other = other.Previous; - if (other == null) break; - ++interval; - } - - RepetitionInterval = max_repetition_interval + 1; - } - } -} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 6b7138fa92..ba4c066a67 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills @@ -15,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// public class Colour : StrainDecaySkill { - protected override double SkillMultiplier => 1; + protected override double SkillMultiplier => 0.7; protected override double StrainDecayBase => 0.4; /// @@ -38,6 +40,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return difficulty; } + public static String GetDebugHeaderLabels() + { + return "StartTime,Raw,Decayed,CoupledRunLength,RepetitionInterval,EncodingRunLength,Payload(MonoRunLength|MonoCount)"; + } + // TODO: Remove befor pr public string GetDebugString(DifficultyHitObject current) { @@ -47,18 +54,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills TaikoDifficultyHitObjectColour? colour = taikoCurrent?.Colour; if (taikoCurrent != null && colour != null) { - ColourEncoding[] payload = colour.Encoding.Payload; + List payload = colour.Encoding.Payload; string payloadDisplay = ""; - for (int i = 0; i < payload.Length; ++i) + for (int i = 0; i < payload.Count; ++i) { - payloadDisplay += $"({payload[i].MonoRunLength}|{payload[i].EncodingRunLength})"; + payloadDisplay += $"({payload[i].Payload[0].RunLength}|{payload[i].Payload.Count})"; } - return $"{current.StartTime},{difficulty},{CurrentStrain},{colour.RepetitionInterval},{colour.Encoding.RunLength},{payloadDisplay}"; + return $"{current.StartTime},{difficulty},{CurrentStrain},{colour.Encoding.Payload[0].Payload.Count},{colour.Encoding.RepetitionInterval},{colour.Encoding.Payload.Count},{payloadDisplay}"; } else { - return $"{current.StartTime},{difficulty},{CurrentStrain},0,0,0"; + return $"{current.StartTime},{difficulty},{CurrentStrain},0,0,0,0,0"; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs index 1086cf5f72..434474055e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs @@ -38,10 +38,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills if (debugColour) { - String filename = $"{beatmap.BeatmapInfo.Metadata.Title}[{beatmap.BeatmapInfo.DifficultyName}].csv"; + String filename = $"{beatmap.BeatmapInfo.OnlineID} - {beatmap.BeatmapInfo.Metadata.Title}[{beatmap.BeatmapInfo.DifficultyName}].csv"; filename = filename.Replace('/', '_'); colourDebugOutput = new StreamWriter(File.OpenWrite($"/run/mount/secondary/workspace/osu/output/colour-debug/{filename}")); - colourDebugOutput.WriteLine("StartTime,Raw,Decayed,RepetitionInterval,EncodingRunLength,Payload"); + colourDebugOutput.WriteLine(Colour.GetDebugHeaderLabels()); } } From f6dedc77fbc892339c642b6aa154e69ae3dd206b Mon Sep 17 00:00:00 2001 From: vun Date: Tue, 5 Jul 2022 17:01:11 +0800 Subject: [PATCH 035/532] Fixed encoding logic, parameter adjustments --- .../Difficulty/Preprocessing/Colour/ColourEncoding.cs | 3 ++- osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs | 4 ++-- osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs index bc427d87ae..47523189e1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs @@ -9,7 +9,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour public bool isIdenticalTo(ColourEncoding other) { - return other.Payload[0].RunLength == Payload[0].RunLength && + return hasIdenticalMonoLength(other) && + other.Payload.Count == Payload.Count && other.Payload[0].EncodedData[0].HitType == Payload[0].EncodedData[0].HitType; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index ba4c066a67..5f9185a547 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// public class Colour : StrainDecaySkill { - protected override double SkillMultiplier => 0.7; - protected override double StrainDecayBase => 0.4; + protected override double SkillMultiplier => 0.2; + protected override double StrainDecayBase => 0.8; /// /// Applies a speed bonus dependent on the time since the last hit. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs index 434474055e..16e5306d5d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs @@ -12,8 +12,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public class Peaks : Skill { private const double rhythm_skill_multiplier = 0.3 * final_multiplier; - private const double colour_skill_multiplier = 0.375 * final_multiplier; - private const double stamina_skill_multiplier = 0.375 * final_multiplier; + private const double colour_skill_multiplier = 0.4 * final_multiplier; + private const double stamina_skill_multiplier = 0.35 * final_multiplier; private const double final_multiplier = 0.06; From 6660379a0eedb15fef1b6fccedcbd58b8c4cfd98 Mon Sep 17 00:00:00 2001 From: vun Date: Thu, 7 Jul 2022 16:04:46 +0800 Subject: [PATCH 036/532] TAIKO-6 Tweak encoding and parameters, reduce rhythm weight --- .../Difficulty/Evaluators/ColourEvaluator.cs | 4 +- .../Difficulty/Evaluators/StaminaEvaluator.cs | 4 +- .../Preprocessing/Colour/ColourEncoding.cs | 2 +- .../Colour/CoupledColourEncoding.cs | 4 +- .../Difficulty/Skills/Colour.cs | 58 +++++++++---------- .../Difficulty/Skills/Peaks.cs | 36 ++++++------ .../Difficulty/Skills/Stamina.cs | 2 +- 7 files changed, 54 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index d33f2a85b9..388858a337 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// public static void PreEvaluateDifficulties(CoupledColourEncoding encoding) { - double coupledEncodingDifficulty = EvaluateDifficultyOf(encoding); + double coupledEncodingDifficulty = 2 * EvaluateDifficultyOf(encoding); encoding.Payload[0].Payload[0].EncodedData[0].Colour!.EvaluatedDifficulty += coupledEncodingDifficulty; for (int i = 0; i < encoding.Payload.Count; i++) { @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators for (int j = 0; j < colourEncoding.Payload.Count; j++) { MonoEncoding monoEncoding = colourEncoding.Payload[j]; - monoEncoding.EncodedData[0].Colour!.EvaluatedDifficulty += EvaluateDifficultyOf(monoEncoding, j) * colourEncodingDifficulty; + monoEncoding.EncodedData[0].Colour!.EvaluatedDifficulty += EvaluateDifficultyOf(monoEncoding, j) * colourEncodingDifficulty * 0.5; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index 9ebdc90eb8..6889f0f5e9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -16,8 +16,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// The interval between the current and previous note hit using the same key. private static double speedBonus(double interval) { - // return 10 / Math.Pow(interval, 0.6); - return Math.Pow(0.1, interval / 1000); + // return 15 / Math.Pow(interval, 0.6); + return Math.Pow(0.2, interval / 1000); } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs index 47523189e1..18f9d4cf7d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs @@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour { public List Payload { get; private set; } = new List(); - public bool isIdenticalTo(ColourEncoding other) + public bool isRepetitionOf(ColourEncoding other) { return hasIdenticalMonoLength(other) && other.Payload.Count == Payload.Count && diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs index 7bdcf50055..83fd75efa0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour Previous = lastEncoded }; - bool isCoupled = i < data.Count - 2 && data[i].isIdenticalTo(data[i + 2]); + bool isCoupled = i < data.Count - 2 && data[i].isRepetitionOf(data[i + 2]); if (!isCoupled) { lastEncoded.Payload.Add(data[i]); @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour lastEncoded.Payload.Add(data[i]); i++; - isCoupled = i < data.Count - 2 && data[i].isIdenticalTo(data[i + 2]); + isCoupled = i < data.Count - 2 && data[i].isRepetitionOf(data[i + 2]); } // Skip over peeked data and add the rest to the payload diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 5f9185a547..d8f445f37c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -2,12 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills @@ -17,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// public class Colour : StrainDecaySkill { - protected override double SkillMultiplier => 0.2; + protected override double SkillMultiplier => 0.12; protected override double StrainDecayBase => 0.8; /// @@ -40,33 +37,34 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return difficulty; } - public static String GetDebugHeaderLabels() - { - return "StartTime,Raw,Decayed,CoupledRunLength,RepetitionInterval,EncodingRunLength,Payload(MonoRunLength|MonoCount)"; - } + // TODO: Remove before pr + // public static String GetDebugHeaderLabels() + // { + // return "StartTime,Raw,Decayed,CoupledRunLength,RepetitionInterval,EncodingRunLength,Payload(MonoRunLength|MonoCount)"; + // } - // TODO: Remove befor pr - public string GetDebugString(DifficultyHitObject current) - { - double difficulty = ColourEvaluator.EvaluateDifficultyOf(current); - difficulty *= speedBonus(current.DeltaTime); - TaikoDifficultyHitObject? taikoCurrent = (TaikoDifficultyHitObject)current; - TaikoDifficultyHitObjectColour? colour = taikoCurrent?.Colour; - if (taikoCurrent != null && colour != null) - { - List payload = colour.Encoding.Payload; - string payloadDisplay = ""; - for (int i = 0; i < payload.Count; ++i) - { - payloadDisplay += $"({payload[i].Payload[0].RunLength}|{payload[i].Payload.Count})"; - } + // // TODO: Remove before pr + // public string GetDebugString(DifficultyHitObject current) + // { + // double difficulty = ColourEvaluator.EvaluateDifficultyOf(current); + // difficulty *= speedBonus(current.DeltaTime); + // TaikoDifficultyHitObject? taikoCurrent = (TaikoDifficultyHitObject)current; + // TaikoDifficultyHitObjectColour? colour = taikoCurrent?.Colour; + // if (taikoCurrent != null && colour != null) + // { + // List payload = colour.Encoding.Payload; + // string payloadDisplay = ""; + // for (int i = 0; i < payload.Count; ++i) + // { + // payloadDisplay += $"({payload[i].Payload[0].RunLength}|{payload[i].Payload.Count})"; + // } - return $"{current.StartTime},{difficulty},{CurrentStrain},{colour.Encoding.Payload[0].Payload.Count},{colour.Encoding.RepetitionInterval},{colour.Encoding.Payload.Count},{payloadDisplay}"; - } - else - { - return $"{current.StartTime},{difficulty},{CurrentStrain},0,0,0,0,0"; - } - } + // return $"{current.StartTime},{difficulty},{CurrentStrain},{colour.Encoding.Payload[0].Payload.Count},{colour.Encoding.RepetitionInterval},{colour.Encoding.Payload.Count},{payloadDisplay}"; + // } + // else + // { + // return $"{current.StartTime},{difficulty},{CurrentStrain},0,0,0,0,0"; + // } + // } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs index 16e5306d5d..09d9720a4f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs @@ -11,11 +11,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Peaks : Skill { - private const double rhythm_skill_multiplier = 0.3 * final_multiplier; - private const double colour_skill_multiplier = 0.4 * final_multiplier; - private const double stamina_skill_multiplier = 0.35 * final_multiplier; + private const double rhythm_skill_multiplier = 0.2 * final_multiplier; + private const double colour_skill_multiplier = 0.375 * final_multiplier; + private const double stamina_skill_multiplier = 0.375 * final_multiplier; - private const double final_multiplier = 0.06; + private const double final_multiplier = 0.0625; private readonly Rhythm rhythm; private readonly Colour colour; @@ -26,8 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public double StaminaDifficultyValue => stamina.DifficultyValue() * stamina_skill_multiplier; // TODO: remove before pr - private StreamWriter? colourDebugOutput; - bool debugColour = false; + // private StreamWriter? colourDebugOutput; + // bool debugColour = false; public Peaks(Mod[] mods, IBeatmap beatmap) : base(mods) @@ -36,13 +36,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills colour = new Colour(mods); stamina = new Stamina(mods); - if (debugColour) - { - String filename = $"{beatmap.BeatmapInfo.OnlineID} - {beatmap.BeatmapInfo.Metadata.Title}[{beatmap.BeatmapInfo.DifficultyName}].csv"; - filename = filename.Replace('/', '_'); - colourDebugOutput = new StreamWriter(File.OpenWrite($"/run/mount/secondary/workspace/osu/output/colour-debug/{filename}")); - colourDebugOutput.WriteLine(Colour.GetDebugHeaderLabels()); - } + // if (debugColour) + // { + // String filename = $"{beatmap.BeatmapInfo.OnlineID} - {beatmap.BeatmapInfo.Metadata.Title}[{beatmap.BeatmapInfo.DifficultyName}].csv"; + // filename = filename.Replace('/', '_'); + // colourDebugOutput = new StreamWriter(File.OpenWrite($"/run/mount/secondary/workspace/osu/output/colour-debug/{filename}")); + // colourDebugOutput.WriteLine(Colour.GetDebugHeaderLabels()); + // } } @@ -59,11 +59,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills colour.Process(current); stamina.Process(current); - if (debugColour && colourDebugOutput != null) - { - colourDebugOutput.WriteLine(colour.GetDebugString(current)); - colourDebugOutput.Flush(); - } + // if (debugColour && colourDebugOutput != null) + // { + // colourDebugOutput.WriteLine(colour.GetDebugString(current)); + // colourDebugOutput.Flush(); + // } } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 777bd97f81..344004bcf6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// public class Stamina : StrainDecaySkill { - protected override double SkillMultiplier => 1.2; + protected override double SkillMultiplier => 1.1; protected override double StrainDecayBase => 0.4; /// From 4fdbf3ff247921129865b79236596bf2b1f6e66e Mon Sep 17 00:00:00 2001 From: 63411 <62799417+molneya@users.noreply.github.com> Date: Thu, 14 Jul 2022 15:07:58 +0800 Subject: [PATCH 037/532] individualStrain should be the hardest individualStrain column for notes in a chord --- osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index 74334c90e4..657dfbb575 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -78,7 +78,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base); individualStrains[column] += 2.0 * holdFactor; - individualStrain = individualStrains[column]; + // individualStrain should be the hardest individualStrain column for notes in a chord + individualStrain = maniaCurrent.DeltaTime == 0 ? Math.Max(individualStrain, individualStrains[column]) : individualStrains[column]; // Decay and increase overallStrain overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base); From 1cb18f84743840a43ced5cc5cae9f9cce78807e1 Mon Sep 17 00:00:00 2001 From: vun Date: Thu, 14 Jul 2022 16:29:23 +0800 Subject: [PATCH 038/532] Refactor colour encoding to avoid circular dependencies --- .../Difficulty/Evaluators/ColourEvaluator.cs | 12 -- .../Difficulty/Evaluators/StaminaEvaluator.cs | 14 +- .../Preprocessing/Colour/ColourEncoding.cs | 41 ++--- .../Colour/CoupledColourEncoding.cs | 75 ++------ .../Preprocessing/Colour/MonoEncoding.cs | 38 ++-- .../TaikoColourDifficultyPreprocessor.cs | 170 ++++++++++++++++++ .../Colour/TaikoDifficultyHitObjectColour.cs | 34 +--- .../Preprocessing/TaikoDifficultyHitObject.cs | 49 +---- .../TaikoDifficultyPreprocessor.cs | 38 ++++ .../Difficulty/Skills/Colour.cs | 70 ++++---- .../Difficulty/Skills/Peaks.cs | 33 ++-- .../Difficulty/TaikoDifficultyCalculator.cs | 2 +- 12 files changed, 326 insertions(+), 250 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 388858a337..2193184355 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -1,5 +1,4 @@ using System; -using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; @@ -67,16 +66,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators } } } - - public static double EvaluateDifficultyOf(DifficultyHitObject current) - { - TaikoDifficultyHitObject? taikoObject = current as TaikoDifficultyHitObject; - if (taikoObject != null && taikoObject.Colour != null) - { - return taikoObject.Colour.EvaluatedDifficulty; - } - - return 0; - } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index 6889f0f5e9..e47e131350 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -16,8 +16,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// The interval between the current and previous note hit using the same key. private static double speedBonus(double interval) { + // Cap to 300bpm 1/4, 50ms note interval, 100ms key interval + // This is a temporary measure to prevent absurdly high speed mono convert maps being rated too high + // There is a plan to replace this with detecting mono that can be hit by special techniques, and this will + // be removed when that is implemented. + interval = Math.Max(interval, 100); + // return 15 / Math.Pow(interval, 0.6); - return Math.Pow(0.2, interval / 1000); + // return Math.Pow(0.2, interval / 1000); + return 30 / interval; } /// @@ -41,8 +48,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators return 0.0; } - double objectStrain = 1; - objectStrain *= speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime); + double objectStrain = 0.5; // Add a base strain to all objects + // double objectStrain = 0; + objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime); return objectStrain; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs index 18f9d4cf7d..052af7a2d1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs @@ -1,43 +1,38 @@ using System.Collections.Generic; -using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour { + /// + /// Encodes a list of s. + /// s with the same are grouped together. + /// public class ColourEncoding { + /// + /// s that are grouped together within this . + /// public List Payload { get; private set; } = new List(); + /// + /// Determine if this is a repetition of another . This + /// is a strict comparison and is true if and only if the colour sequence is exactly the same. + /// This does not require the s to have the same amount of s. + /// public bool isRepetitionOf(ColourEncoding other) { return hasIdenticalMonoLength(other) && other.Payload.Count == Payload.Count && - other.Payload[0].EncodedData[0].HitType == Payload[0].EncodedData[0].HitType; + (other.Payload[0].EncodedData[0].BaseObject as Hit)?.Type == + (Payload[0].EncodedData[0].BaseObject as Hit)?.Type; } + /// + /// Determine if this has the same mono length of another . + /// public bool hasIdenticalMonoLength(ColourEncoding other) { return other.Payload[0].RunLength == Payload[0].RunLength; } - - public static List Encode(List data) - { - // Compute encoding lengths - List encoded = new List(); - ColourEncoding? lastEncoded = null; - for (int i = 0; i < data.Count; i++) - { - if (i == 0 || lastEncoded == null || data[i].RunLength != data[i - 1].RunLength) - { - lastEncoded = new ColourEncoding(); - lastEncoded.Payload.Add(data[i]); - encoded.Add(lastEncoded); - continue; - } - - lastEncoded.Payload.Add(data[i]); - } - - return encoded; - } } } \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs index 83fd75efa0..85a5f14a5d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs @@ -1,78 +1,35 @@ using System; using System.Collections.Generic; -using osu.Game.Rulesets.Difficulty.Preprocessing; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour { + /// + /// Encodes a list of s, grouped together by back and forth repetition of the same + /// . Also stores the repetition interval between this and the previous . + /// public class CoupledColourEncoding { + /// + /// Maximum amount of s to look back to find a repetition. + /// private const int max_repetition_interval = 16; + /// + /// The s that are grouped together within this . + /// public List Payload = new List(); - public CoupledColourEncoding? Previous { get; private set; } = null; + /// + /// The previous . This is used to determine the repetition interval. + /// + public CoupledColourEncoding? Previous = null; /// - /// How many notes between the current and previous identical . - /// Negative number means that there is no repetition in range. + /// How many between the current and previous identical . /// If no repetition is found this will have a value of + 1. /// public int RepetitionInterval { get; private set; } = max_repetition_interval + 1; - public static List Encode(List data) - { - List firstPass = MonoEncoding.Encode(data); - List secondPass = ColourEncoding.Encode(firstPass); - List thirdPass = CoupledColourEncoding.Encode(secondPass); - - return thirdPass; - } - - public static List Encode(List data) - { - List encoded = new List(); - - CoupledColourEncoding? lastEncoded = null; - for (int i = 0; i < data.Count; i++) - { - lastEncoded = new CoupledColourEncoding() - { - Previous = lastEncoded - }; - - bool isCoupled = i < data.Count - 2 && data[i].isRepetitionOf(data[i + 2]); - if (!isCoupled) - { - lastEncoded.Payload.Add(data[i]); - } - else - { - while (isCoupled) - { - lastEncoded.Payload.Add(data[i]); - i++; - - isCoupled = i < data.Count - 2 && data[i].isRepetitionOf(data[i + 2]); - } - - // Skip over peeked data and add the rest to the payload - lastEncoded.Payload.Add(data[i]); - lastEncoded.Payload.Add(data[i + 1]); - i++; - } - - encoded.Add(lastEncoded); - } - - // Final pass to find repetition interval - for (int i = 0; i < encoded.Count; i++) - { - encoded[i].FindRepetitionInterval(); - } - - return encoded; - } - /// /// Returns true if other is considered a repetition of this encoding. This is true if other's first two payload /// identical mono lengths. @@ -90,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour } /// - /// Finds the closest previous that has the identical . + /// Finds the closest previous that has the identical . /// Interval is defined as the amount of chunks between the current and repeated encoding. /// public void FindRepetitionInterval() diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/MonoEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/MonoEncoding.cs index 92cdb0667b..98d66f0aa2 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/MonoEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/MonoEncoding.cs @@ -1,40 +1,22 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; using System.Collections.Generic; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour { + /// + /// Encode colour information for a sequence of s. Consecutive s + /// of the same are encoded within the same . + /// public class MonoEncoding { + /// + /// List of s that are encoded within this . + /// This is not declared as to avoid circular dependencies. + /// TODO: Review this, are circular dependencies within data-only classes are acceptable? + /// public List EncodedData { get; private set; } = new List(); public int RunLength => EncodedData.Count; - - public static List Encode(List data) - { - List encoded = new List(); - - MonoEncoding? lastEncoded = null; - for (int i = 0; i < data.Count; i++) - { - TaikoDifficultyHitObject taikoObject = (TaikoDifficultyHitObject)data[i]; - // This ignores all non-note objects, which may or may not be the desired behaviour - TaikoDifficultyHitObject previousObject = (TaikoDifficultyHitObject)taikoObject.PreviousNote(0); - - if ( - previousObject == null || - lastEncoded == null || - taikoObject.HitType != previousObject.HitType) - { - lastEncoded = new MonoEncoding(); - lastEncoded.EncodedData.Add(taikoObject); - encoded.Add(lastEncoded); - continue; - } - - lastEncoded.EncodedData.Add(taikoObject); - } - - return encoded; - } } } \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs new file mode 100644 index 0000000000..17337281e2 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -0,0 +1,170 @@ +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour +{ + /// + /// Utility class to perform various encodings. This is separated out from the encoding classes to prevent circular + /// dependencies. + /// + public class TaikoColourDifficultyPreprocessor + { + /// + /// Process and encode a list of s into a list of s, + /// assign the appropriate s to each , + /// and preevaluate colour difficulty of each . + /// + public static List ProcessAndAssign(List hitObjects) + { + List colours = new List(); + List encodings = Encode(hitObjects); + + // Assign colour to objects + encodings.ForEach(coupledEncoding => + { + coupledEncoding.Payload.ForEach(encoding => + { + encoding.Payload.ForEach(mono => + { + mono.EncodedData.ForEach(hitObject => + { + hitObject.Colour = new TaikoDifficultyHitObjectColour(coupledEncoding); + }); + }); + }); + + // Preevaluate and assign difficulty values + ColourEvaluator.PreEvaluateDifficulties(coupledEncoding); + }); + + return colours; + } + + /// + /// Encodes a list of s into a list of s. + /// + public static List EncodeMono(List data) + { + List encoded = new List(); + + MonoEncoding? lastEncoded = null; + for (int i = 0; i < data.Count; i++) + { + TaikoDifficultyHitObject taikoObject = (TaikoDifficultyHitObject)data[i]; + // This ignores all non-note objects, which may or may not be the desired behaviour + TaikoDifficultyHitObject previousObject = taikoObject.PreviousNote(0); + + // If the colour changed, or if this is the first object in the run, create a new mono encoding + if ( + previousObject == null || // First object in the list + (taikoObject.BaseObject as Hit)?.Type != (previousObject.BaseObject as Hit)?.Type) + { + lastEncoded = new MonoEncoding(); + lastEncoded.EncodedData.Add(taikoObject); + encoded.Add(lastEncoded); + continue; + } + + // If we're here, we're in the same encoding as the previous object, thus lastEncoded is not null. Add + // the current object to the encoded payload. + lastEncoded!.EncodedData.Add(taikoObject); + } + + return encoded; + } + + /// + /// Encodes a list of s into a list of s. + /// + public static List EncodeColour(List data) + { + List encoded = new List(); + ColourEncoding? lastEncoded = null; + for (int i = 0; i < data.Count; i++) + { + // Starts a new ColourEncoding if the previous MonoEncoding has a different mono length, or if this is + // the first MonoEncoding in the list. + if (lastEncoded == null || data[i].RunLength != data[i - 1].RunLength) + { + lastEncoded = new ColourEncoding(); + lastEncoded.Payload.Add(data[i]); + encoded.Add(lastEncoded); + continue; + } + + // If we're here, we're in the same encoding as the previous object. Add the current MonoEncoding to the + // encoded payload. + lastEncoded.Payload.Add(data[i]); + } + + return encoded; + } + + /// + /// Encodes a list of s into a list of s. + /// + public static List Encode(List data) + { + List firstPass = EncodeMono(data); + List secondPass = EncodeColour(firstPass); + List thirdPass = EncodeCoupledColour(secondPass); + + return thirdPass; + } + + /// + /// Encodes a list of s into a list of s. + /// + public static List EncodeCoupledColour(List data) + { + List encoded = new List(); + CoupledColourEncoding? lastEncoded = null; + for (int i = 0; i < data.Count; i++) + { + // Starts a new CoupledColourEncoding. ColourEncodings that should be grouped together will be handled + // later within this loop. + lastEncoded = new CoupledColourEncoding() + { + Previous = lastEncoded + }; + + // Determine if future ColourEncodings should be grouped. + bool isCoupled = i < data.Count - 2 && data[i].isRepetitionOf(data[i + 2]); + if (!isCoupled) + { + // If not, add the current ColourEncoding to the encoded payload and continue. + lastEncoded.Payload.Add(data[i]); + } + else + { + // If so, add the current ColourEncoding to the encoded payload and start repeatedly checking if + // subsequent ColourEncodings should be grouped by increasing i and doing the appropriate isCoupled + // check; + while (isCoupled) + { + lastEncoded.Payload.Add(data[i]); + i++; + isCoupled = i < data.Count - 2 && data[i].isRepetitionOf(data[i + 2]); + } + + // Skip over peeked data and add the rest to the payload + lastEncoded.Payload.Add(data[i]); + lastEncoded.Payload.Add(data[i + 1]); + i++; + } + + encoded.Add(lastEncoded); + } + + // Final pass to find repetition intervals + for (int i = 0; i < encoded.Count; i++) + { + encoded[i].FindRepetitionInterval(); + } + + return encoded; + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs index 7dfd0fdbd4..9c2e0cc206 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; - namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour { /// @@ -15,36 +10,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour public double EvaluatedDifficulty = 0; - private TaikoDifficultyHitObjectColour(CoupledColourEncoding encoding) + public TaikoDifficultyHitObjectColour(CoupledColourEncoding encoding) { Encoding = encoding; } - - // TODO: Might wanna move this somewhere else as it is introducing circular references - public static List EncodeAndAssign(List hitObjects) - { - List colours = new List(); - List encodings = CoupledColourEncoding.Encode(hitObjects); - - // Assign colour to objects - encodings.ForEach(coupledEncoding => - { - coupledEncoding.Payload.ForEach(encoding => - { - encoding.Payload.ForEach(mono => - { - mono.EncodedData.ForEach(hitObject => - { - hitObject.Colour = new TaikoDifficultyHitObjectColour(coupledEncoding); - }); - }); - }); - - // Preevaluate and assign difficulty values - ColourEvaluator.PreEvaluateDifficulties(coupledEncoding); - }); - - return colours; - } } } \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 919770afc8..e490d310fd 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -4,12 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; -using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { @@ -35,40 +33,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public TaikoDifficultyHitObjectColour? Colour; - /// - /// The hit type of this hit object. - /// - public readonly HitType? HitType; - - /// - /// Creates a list of s from a s. - /// TODO: Review this - this is moved here from TaikoDifficultyCalculator so that TaikoDifficultyCalculator can - /// have less knowledge of implementation details (i.e. creating all the different hitObject lists, and - /// calling FindRepetitionInterval for the final object). The down side of this is - /// TaikoDifficultyHitObejct.CreateDifficultyHitObjects is now pretty much a proxy for this. - /// - /// The beatmap from which the list of is created. - /// The rate at which the gameplay clock is run at. - public static List Create(IBeatmap beatmap, double clockRate) - { - List difficultyHitObjects = new List(); - List centreObjects = new List(); - List rimObjects = new List(); - List noteObjects = new List(); - - for (int i = 2; i < beatmap.HitObjects.Count; i++) - { - difficultyHitObjects.Add( - new TaikoDifficultyHitObject( - beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects, - centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count) - ); - } - var encoded = TaikoDifficultyHitObjectColour.EncodeAndAssign(difficultyHitObjects); - - return difficultyHitObjects; - } - /// /// Creates a new difficulty hit object. /// @@ -81,10 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// The list of rim (kat) s in the current beatmap. /// The list of s that is a hit (i.e. not a slider or spinner) in the current beatmap. /// The position of this in the list. - /// - /// TODO: This argument list is getting long, we might want to refactor this into a static method that create - /// all s from a . - private TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, List objects, List centreHitObjects, List rimHitObjects, @@ -95,15 +56,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing this.noteObjects = noteObjects; Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate); - HitType = currentHit?.Type; + HitType? hitType = currentHit?.Type; - if (HitType == Objects.HitType.Centre) + if (hitType == Objects.HitType.Centre) { MonoIndex = centreHitObjects.Count; centreHitObjects.Add(this); monoDifficultyHitObjects = centreHitObjects; } - else if (HitType == Objects.HitType.Rim) + else if (hitType == Objects.HitType.Rim) { MonoIndex = rimHitObjects.Count; rimHitObjects.Add(this); @@ -111,7 +72,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } // Need to be done after HitType is set. - if (HitType == null) return; + if (hitType == null) return; NoteIndex = noteObjects.Count; noteObjects.Add(this); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs new file mode 100644 index 0000000000..e37690d7e8 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing +{ + public class TaikoDifficultyPreprocessor + { + /// + /// Creates a list of s from a s. + /// This is placed here in a separate class to avoid having to know + /// too much implementation details of the preprocessing, and avoid + /// having circular dependencies with various preprocessing and evaluator classes. + /// + /// The beatmap from which the list of is created. + /// The rate at which the gameplay clock is run at. + public static List CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) + { + List difficultyHitObjects = new List(); + List centreObjects = new List(); + List rimObjects = new List(); + List noteObjects = new List(); + + for (int i = 2; i < beatmap.HitObjects.Count; i++) + { + difficultyHitObjects.Add( + new TaikoDifficultyHitObject( + beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects, + centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count) + ); + } + var encoded = TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); + + return difficultyHitObjects; + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index d8f445f37c..e2147fdd85 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { @@ -15,16 +17,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public class Colour : StrainDecaySkill { protected override double SkillMultiplier => 0.12; - protected override double StrainDecayBase => 0.8; - /// - /// Applies a speed bonus dependent on the time since the last hit. - /// - /// The interval between the current and previous note hit using the same key. - private static double speedBonus(double interval) - { - return Math.Pow(0.4, interval / 1000); - } + // This is set to decay slower than other skills, due to the fact that only the first note of each Mono/Colour/Coupled + // encoding having any difficulty values, and we want to allow colour difficulty to be able to build up even on + // slower maps. + protected override double StrainDecayBase => 0.8; public Colour(Mod[] mods) : base(mods) @@ -33,38 +30,37 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double StrainValueOf(DifficultyHitObject current) { - double difficulty = ColourEvaluator.EvaluateDifficultyOf(current); + double difficulty = ((TaikoDifficultyHitObject)current).Colour?.EvaluatedDifficulty ?? 0; return difficulty; } // TODO: Remove before pr - // public static String GetDebugHeaderLabels() - // { - // return "StartTime,Raw,Decayed,CoupledRunLength,RepetitionInterval,EncodingRunLength,Payload(MonoRunLength|MonoCount)"; - // } + public static String GetDebugHeaderLabels() + { + return "StartTime,Raw,Decayed,CoupledRunLength,RepetitionInterval,EncodingRunLength,Payload(MonoRunLength|MonoCount)"; + } - // // TODO: Remove before pr - // public string GetDebugString(DifficultyHitObject current) - // { - // double difficulty = ColourEvaluator.EvaluateDifficultyOf(current); - // difficulty *= speedBonus(current.DeltaTime); - // TaikoDifficultyHitObject? taikoCurrent = (TaikoDifficultyHitObject)current; - // TaikoDifficultyHitObjectColour? colour = taikoCurrent?.Colour; - // if (taikoCurrent != null && colour != null) - // { - // List payload = colour.Encoding.Payload; - // string payloadDisplay = ""; - // for (int i = 0; i < payload.Count; ++i) - // { - // payloadDisplay += $"({payload[i].Payload[0].RunLength}|{payload[i].Payload.Count})"; - // } + // TODO: Remove before pr + public string GetDebugString(DifficultyHitObject current) + { + double difficulty = ((TaikoDifficultyHitObject)current).Colour?.EvaluatedDifficulty ?? 0; + TaikoDifficultyHitObject? taikoCurrent = (TaikoDifficultyHitObject)current; + TaikoDifficultyHitObjectColour? colour = taikoCurrent?.Colour; + if (taikoCurrent != null && colour != null) + { + List payload = colour.Encoding.Payload; + string payloadDisplay = ""; + for (int i = 0; i < payload.Count; ++i) + { + payloadDisplay += $"({payload[i].Payload[0].RunLength}|{payload[i].Payload.Count})"; + } - // return $"{current.StartTime},{difficulty},{CurrentStrain},{colour.Encoding.Payload[0].Payload.Count},{colour.Encoding.RepetitionInterval},{colour.Encoding.Payload.Count},{payloadDisplay}"; - // } - // else - // { - // return $"{current.StartTime},{difficulty},{CurrentStrain},0,0,0,0,0"; - // } - // } + return $"{current.StartTime},{difficulty},{CurrentStrain},{colour.Encoding.Payload[0].Payload.Count},{colour.Encoding.RepetitionInterval},{colour.Encoding.Payload.Count},{payloadDisplay}"; + } + else + { + return $"{current.StartTime},{difficulty},{CurrentStrain},0,0,0,0,0"; + } + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs index 09d9720a4f..18200fe1bb 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs @@ -26,8 +26,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public double StaminaDifficultyValue => stamina.DifficultyValue() * stamina_skill_multiplier; // TODO: remove before pr - // private StreamWriter? colourDebugOutput; - // bool debugColour = false; + private StreamWriter? colourDebugOutput; + bool debugColour = false; + private StreamWriter? strainPeakDebugOutput; + bool debugStrain = false; public Peaks(Mod[] mods, IBeatmap beatmap) : base(mods) @@ -36,14 +38,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills colour = new Colour(mods); stamina = new Stamina(mods); - // if (debugColour) - // { - // String filename = $"{beatmap.BeatmapInfo.OnlineID} - {beatmap.BeatmapInfo.Metadata.Title}[{beatmap.BeatmapInfo.DifficultyName}].csv"; - // filename = filename.Replace('/', '_'); - // colourDebugOutput = new StreamWriter(File.OpenWrite($"/run/mount/secondary/workspace/osu/output/colour-debug/{filename}")); - // colourDebugOutput.WriteLine(Colour.GetDebugHeaderLabels()); - // } - + if (debugColour) + { + String filename = $"{beatmap.BeatmapInfo.OnlineID} - {beatmap.BeatmapInfo.Metadata.Title}[{beatmap.BeatmapInfo.DifficultyName}].csv"; + filename = filename.Replace('/', '_'); + colourDebugOutput = new StreamWriter(File.OpenWrite($"/run/mount/secondary/workspace/osu/output/colour-debug/{filename}")); + colourDebugOutput.WriteLine(Colour.GetDebugHeaderLabels()); + } + if (debugStrain) + { + String filename = $"{beatmap.BeatmapInfo.OnlineID} - {beatmap.BeatmapInfo.Metadata.Title}[{beatmap.BeatmapInfo.DifficultyName}].csv"; + filename = filename.Replace('/', '_'); + strainPeakDebugOutput = new StreamWriter(File.OpenWrite($"/run/mount/secondary/workspace/osu/output/strain-debug/{filename}")); + strainPeakDebugOutput.WriteLine("Colour,Stamina,Rhythm,Combined"); + } } /// @@ -90,6 +98,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills double peak = norm(1.5, colourPeak, staminaPeak); peak = norm(2, peak, rhythmPeak); + if (debugStrain && strainPeakDebugOutput != null) + { + strainPeakDebugOutput.WriteLine($"{colourPeak},{staminaPeak},{rhythmPeak},{peak}"); + } + // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // These sections will not contribute to the difficulty. if (peak > 0) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 2982861e0b..195ec92835 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - return TaikoDifficultyHitObject.Create(beatmap, clockRate); + return TaikoDifficultyPreprocessor.CreateDifficultyHitObjects(beatmap, clockRate); } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) From 45c055bfa1f4f5f569fc4c1d24582ee322f0076a Mon Sep 17 00:00:00 2001 From: vun Date: Thu, 14 Jul 2022 17:25:21 +0800 Subject: [PATCH 039/532] Move rhythm preprocessing to its own folder --- .../{ => Rhythm}/TaikoDifficultyHitObjectRhythm.cs | 2 +- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/{ => Rhythm}/TaikoDifficultyHitObjectRhythm.cs (95%) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs similarity index 95% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs index 526d20e7d7..bf136b9fa6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs @@ -3,7 +3,7 @@ #nullable disable -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm { /// /// Represents a rhythm change in a taiko map. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index e490d310fd..1b5de64ed3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { From 7e3f62a5a54000d74f0ad4b1ec5f69f74ac96cb5 Mon Sep 17 00:00:00 2001 From: Jay L Date: Fri, 15 Jul 2022 21:07:01 +1000 Subject: [PATCH 040/532] Codequality parse --- .../Difficulty/Evaluators/ColourEvaluator.cs | 7 +++- .../Difficulty/Evaluators/StaminaEvaluator.cs | 7 +--- .../Colour/{ => Data}/ColourEncoding.cs | 19 +++++---- .../{ => Data}/CoupledColourEncoding.cs | 21 ++++++---- .../Colour/{ => Data}/MonoEncoding.cs | 7 +++- .../TaikoColourDifficultyPreprocessor.cs | 30 +++++++++----- .../Colour/TaikoDifficultyHitObjectColour.cs | 9 +++- .../Rhythm/TaikoDifficultyHitObjectRhythm.cs | 2 - .../Preprocessing/TaikoDifficultyHitObject.cs | 20 ++++----- .../TaikoDifficultyPreprocessor.cs | 8 +++- .../Difficulty/Skills/Colour.cs | 34 +-------------- .../Difficulty/Skills/Peaks.cs | 41 +++---------------- .../Difficulty/TaikoDifficultyCalculator.cs | 2 +- 13 files changed, 89 insertions(+), 118 deletions(-) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/{ => Data}/ColourEncoding.cs (70%) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/{ => Data}/CoupledColourEncoding.cs (83%) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/{ => Data}/MonoEncoding.cs (86%) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 2193184355..1d857a1dbb 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -1,12 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data; namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { public class ColourEvaluator { - // TODO - Share this sigmoid private static double sigmoid(double val, double center, double width) { return Math.Tanh(Math.E * -(val - center) / width); @@ -54,11 +57,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { double coupledEncodingDifficulty = 2 * EvaluateDifficultyOf(encoding); encoding.Payload[0].Payload[0].EncodedData[0].Colour!.EvaluatedDifficulty += coupledEncodingDifficulty; + for (int i = 0; i < encoding.Payload.Count; i++) { ColourEncoding colourEncoding = encoding.Payload[i]; double colourEncodingDifficulty = EvaluateDifficultyOf(colourEncoding, i) * coupledEncodingDifficulty; colourEncoding.Payload[0].EncodedData[0].Colour!.EvaluatedDifficulty += colourEncodingDifficulty; + for (int j = 0; j < colourEncoding.Payload.Count; j++) { MonoEncoding monoEncoding = colourEncoding.Payload[j]; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index e47e131350..30918681f6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -22,8 +22,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators // be removed when that is implemented. interval = Math.Max(interval, 100); - // return 15 / Math.Pow(interval, 0.6); - // return Math.Pow(0.2, interval / 1000); return 30 / interval; } @@ -33,14 +31,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// public static double EvaluateDifficultyOf(DifficultyHitObject current) { - if (!(current.BaseObject is Hit)) + if (current.BaseObject is not Hit) { return 0.0; } // Find the previous hit object hit by the current key, which is two notes of the same colour prior. TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current; - TaikoDifficultyHitObject keyPrevious = taikoCurrent.PreviousMono(1); + TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(1); if (keyPrevious == null) { @@ -49,7 +47,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators } double objectStrain = 0.5; // Add a base strain to all objects - // double objectStrain = 0; objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime); return objectStrain; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs similarity index 70% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs index 052af7a2d1..cddf8816f5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/ColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs @@ -1,7 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System.Collections.Generic; using osu.Game.Rulesets.Taiko.Objects; -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data { /// /// Encodes a list of s. @@ -19,20 +22,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// is a strict comparison and is true if and only if the colour sequence is exactly the same. /// This does not require the s to have the same amount of s. /// - public bool isRepetitionOf(ColourEncoding other) + public bool IsRepetitionOf(ColourEncoding other) { - return hasIdenticalMonoLength(other) && - other.Payload.Count == Payload.Count && - (other.Payload[0].EncodedData[0].BaseObject as Hit)?.Type == - (Payload[0].EncodedData[0].BaseObject as Hit)?.Type; + return HasIdenticalMonoLength(other) && + other.Payload.Count == Payload.Count && + (other.Payload[0].EncodedData[0].BaseObject as Hit)?.Type == + (Payload[0].EncodedData[0].BaseObject as Hit)?.Type; } /// /// Determine if this has the same mono length of another . /// - public bool hasIdenticalMonoLength(ColourEncoding other) + public bool HasIdenticalMonoLength(ColourEncoding other) { return other.Payload[0].RunLength == Payload[0].RunLength; } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs similarity index 83% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs index 85a5f14a5d..188d1b686b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/CoupledColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs @@ -1,10 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System; using System.Collections.Generic; -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data { /// - /// Encodes a list of s, grouped together by back and forth repetition of the same + /// Encodes a list of s, grouped together by back and forth repetition of the same /// . Also stores the repetition interval between this and the previous . /// public class CoupledColourEncoding @@ -34,13 +37,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// Returns true if other is considered a repetition of this encoding. This is true if other's first two payload /// identical mono lengths. /// - public bool isRepetitionOf(CoupledColourEncoding other) + private bool isRepetitionOf(CoupledColourEncoding other) { - if (this.Payload.Count != other.Payload.Count) return false; + if (Payload.Count != other.Payload.Count) return false; - for (int i = 0; i < Math.Min(this.Payload.Count, 2); i++) + for (int i = 0; i < Math.Min(Payload.Count, 2); i++) { - if (!this.Payload[i].hasIdenticalMonoLength(other.Payload[i])) return false; + if (!Payload[i].HasIdenticalMonoLength(other.Payload[i])) return false; } return true; @@ -60,9 +63,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour CoupledColourEncoding? other = Previous.Previous; int interval = 2; + while (interval < max_repetition_interval) { - if (this.isRepetitionOf(other)) + if (isRepetitionOf(other)) { RepetitionInterval = Math.Min(interval, max_repetition_interval); return; @@ -70,10 +74,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour other = other.Previous; if (other == null) break; + ++interval; } RepetitionInterval = max_repetition_interval + 1; } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/MonoEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs similarity index 86% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/MonoEncoding.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs index 98d66f0aa2..9e60946bd1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/MonoEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs @@ -1,8 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; using System.Collections.Generic; -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data { /// /// Encode colour information for a sequence of s. Consecutive s @@ -19,4 +22,4 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour public int RunLength => EncodedData.Count; } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index 17337281e2..a699422d2c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -1,6 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data; using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour @@ -50,16 +54,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour List encoded = new List(); MonoEncoding? lastEncoded = null; + for (int i = 0; i < data.Count; i++) { TaikoDifficultyHitObject taikoObject = (TaikoDifficultyHitObject)data[i]; // This ignores all non-note objects, which may or may not be the desired behaviour - TaikoDifficultyHitObject previousObject = taikoObject.PreviousNote(0); + TaikoDifficultyHitObject? previousObject = taikoObject.PreviousNote(0); // If the colour changed, or if this is the first object in the run, create a new mono encoding - if ( + if + ( previousObject == null || // First object in the list - (taikoObject.BaseObject as Hit)?.Type != (previousObject.BaseObject as Hit)?.Type) + (taikoObject.BaseObject as Hit)?.Type != (previousObject.BaseObject as Hit)?.Type + ) + { lastEncoded = new MonoEncoding(); lastEncoded.EncodedData.Add(taikoObject); @@ -82,6 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour { List encoded = new List(); ColourEncoding? lastEncoded = null; + for (int i = 0; i < data.Count; i++) { // Starts a new ColourEncoding if the previous MonoEncoding has a different mono length, or if this is @@ -121,17 +130,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour { List encoded = new List(); CoupledColourEncoding? lastEncoded = null; + for (int i = 0; i < data.Count; i++) { // Starts a new CoupledColourEncoding. ColourEncodings that should be grouped together will be handled // later within this loop. - lastEncoded = new CoupledColourEncoding() + lastEncoded = new CoupledColourEncoding { Previous = lastEncoded }; // Determine if future ColourEncodings should be grouped. - bool isCoupled = i < data.Count - 2 && data[i].isRepetitionOf(data[i + 2]); + bool isCoupled = i < data.Count - 2 && data[i].IsRepetitionOf(data[i + 2]); + if (!isCoupled) { // If not, add the current ColourEncoding to the encoded payload and continue. @@ -139,14 +150,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour } else { - // If so, add the current ColourEncoding to the encoded payload and start repeatedly checking if - // subsequent ColourEncodings should be grouped by increasing i and doing the appropriate isCoupled - // check; + // If so, add the current ColourEncoding to the encoded payload and start repeatedly checking if the + // subsequent ColourEncodings should be grouped by increasing i and doing the appropriate isCoupled check. while (isCoupled) { lastEncoded.Payload.Add(data[i]); i++; - isCoupled = i < data.Count - 2 && data[i].isRepetitionOf(data[i + 2]); + isCoupled = i < data.Count - 2 && data[i].IsRepetitionOf(data[i + 2]); } // Skip over peeked data and add the rest to the payload @@ -167,4 +177,4 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour return encoded; } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs index 9c2e0cc206..9fc7a1dacb 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs @@ -1,3 +1,8 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data; + namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour { /// @@ -6,7 +11,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// public class TaikoDifficultyHitObjectColour { - public CoupledColourEncoding Encoding { get; private set; } + public CoupledColourEncoding Encoding { get; } public double EvaluatedDifficulty = 0; @@ -15,4 +20,4 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour Encoding = encoding; } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs index bf136b9fa6..a273d7e2ea 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm { /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 1b5de64ed3..a0d2fc7797 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -47,10 +47,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// The list of s that is a hit (i.e. not a slider or spinner) in the current beatmap. /// The position of this in the list. public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, - List objects, - List centreHitObjects, - List rimHitObjects, - List noteObjects, int index) + List objects, + List centreHitObjects, + List rimHitObjects, + List noteObjects, int index) : base(hitObject, lastObject, clockRate, objects, index) { var currentHit = hitObject as Hit; @@ -59,13 +59,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate); HitType? hitType = currentHit?.Type; - if (hitType == Objects.HitType.Centre) + if (hitType == HitType.Centre) { MonoIndex = centreHitObjects.Count; centreHitObjects.Add(this); monoDifficultyHitObjects = centreHitObjects; } - else if (hitType == Objects.HitType.Rim) + else if (hitType == HitType.Rim) { MonoIndex = rimHitObjects.Count; rimHitObjects.Add(this); @@ -116,12 +116,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); } - public TaikoDifficultyHitObject PreviousMono(int backwardsIndex) => monoDifficultyHitObjects.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1)); + public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1)); - public TaikoDifficultyHitObject NextMono(int forwardsIndex) => monoDifficultyHitObjects.ElementAtOrDefault(MonoIndex + (forwardsIndex + 1)); + public TaikoDifficultyHitObject? NextMono(int forwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex + (forwardsIndex + 1)); - public TaikoDifficultyHitObject PreviousNote(int backwardsIndex) => noteObjects.ElementAtOrDefault(NoteIndex - (backwardsIndex + 1)); + public TaikoDifficultyHitObject? PreviousNote(int backwardsIndex) => noteObjects.ElementAtOrDefault(NoteIndex - (backwardsIndex + 1)); - public TaikoDifficultyHitObject NextNote(int forwardsIndex) => noteObjects.ElementAtOrDefault(NoteIndex + (forwardsIndex + 1)); + public TaikoDifficultyHitObject? NextNote(int forwardsIndex) => noteObjects.ElementAtOrDefault(NoteIndex + (forwardsIndex + 1)); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs index e37690d7e8..fa8e6a8a26 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Beatmaps; @@ -30,9 +33,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count) ); } - var encoded = TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); + + TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); return difficultyHitObjects; } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index e2147fdd85..8f8f62d214 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -1,13 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; +#nullable disable + using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { @@ -33,34 +32,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills double difficulty = ((TaikoDifficultyHitObject)current).Colour?.EvaluatedDifficulty ?? 0; return difficulty; } - - // TODO: Remove before pr - public static String GetDebugHeaderLabels() - { - return "StartTime,Raw,Decayed,CoupledRunLength,RepetitionInterval,EncodingRunLength,Payload(MonoRunLength|MonoCount)"; - } - - // TODO: Remove before pr - public string GetDebugString(DifficultyHitObject current) - { - double difficulty = ((TaikoDifficultyHitObject)current).Colour?.EvaluatedDifficulty ?? 0; - TaikoDifficultyHitObject? taikoCurrent = (TaikoDifficultyHitObject)current; - TaikoDifficultyHitObjectColour? colour = taikoCurrent?.Colour; - if (taikoCurrent != null && colour != null) - { - List payload = colour.Encoding.Payload; - string payloadDisplay = ""; - for (int i = 0; i < payload.Count; ++i) - { - payloadDisplay += $"({payload[i].Payload[0].RunLength}|{payload[i].Payload.Count})"; - } - - return $"{current.StartTime},{difficulty},{CurrentStrain},{colour.Encoding.Payload[0].Payload.Count},{colour.Encoding.RepetitionInterval},{colour.Encoding.Payload.Count},{payloadDisplay}"; - } - else - { - return $"{current.StartTime},{difficulty},{CurrentStrain},0,0,0,0,0"; - } - } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs index 18200fe1bb..3e3bc543e1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs @@ -1,11 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + using System; -using System.IO; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; -using osu.Game.Beatmaps; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { @@ -25,33 +28,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public double RhythmDifficultyValue => rhythm.DifficultyValue() * rhythm_skill_multiplier; public double StaminaDifficultyValue => stamina.DifficultyValue() * stamina_skill_multiplier; - // TODO: remove before pr - private StreamWriter? colourDebugOutput; - bool debugColour = false; - private StreamWriter? strainPeakDebugOutput; - bool debugStrain = false; - - public Peaks(Mod[] mods, IBeatmap beatmap) + public Peaks(Mod[] mods) : base(mods) { rhythm = new Rhythm(mods); colour = new Colour(mods); stamina = new Stamina(mods); - - if (debugColour) - { - String filename = $"{beatmap.BeatmapInfo.OnlineID} - {beatmap.BeatmapInfo.Metadata.Title}[{beatmap.BeatmapInfo.DifficultyName}].csv"; - filename = filename.Replace('/', '_'); - colourDebugOutput = new StreamWriter(File.OpenWrite($"/run/mount/secondary/workspace/osu/output/colour-debug/{filename}")); - colourDebugOutput.WriteLine(Colour.GetDebugHeaderLabels()); - } - if (debugStrain) - { - String filename = $"{beatmap.BeatmapInfo.OnlineID} - {beatmap.BeatmapInfo.Metadata.Title}[{beatmap.BeatmapInfo.DifficultyName}].csv"; - filename = filename.Replace('/', '_'); - strainPeakDebugOutput = new StreamWriter(File.OpenWrite($"/run/mount/secondary/workspace/osu/output/strain-debug/{filename}")); - strainPeakDebugOutput.WriteLine("Colour,Stamina,Rhythm,Combined"); - } } /// @@ -66,12 +48,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills rhythm.Process(current); colour.Process(current); stamina.Process(current); - - // if (debugColour && colourDebugOutput != null) - // { - // colourDebugOutput.WriteLine(colour.GetDebugString(current)); - // colourDebugOutput.Flush(); - // } } /// @@ -98,11 +74,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills double peak = norm(1.5, colourPeak, staminaPeak); peak = norm(2, peak, rhythmPeak); - if (debugStrain && strainPeakDebugOutput != null) - { - strainPeakDebugOutput.WriteLine($"{colourPeak},{staminaPeak},{rhythmPeak},{peak}"); - } - // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // These sections will not contribute to the difficulty. if (peak > 0) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 195ec92835..22976d6a0b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { return new Skill[] { - new Peaks(mods, beatmap) + new Peaks(mods) }; } From c8b7902a630f5a2b532ca411d6146f3a37ae640c Mon Sep 17 00:00:00 2001 From: Jay L Date: Fri, 15 Jul 2022 22:10:20 +1000 Subject: [PATCH 041/532] Reintroduce Convert Nerf, Rescale Multiplier --- .../Difficulty/TaikoDifficultyCalculator.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 22976d6a0b..a70d3d9a38 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyCalculator : DifficultyCalculator { - private const double difficulty_multiplier = 1.9; + private const double difficulty_multiplier = 1.35; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) @@ -57,12 +57,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty var combined = (Peaks)skills[0]; - double colourRating = Math.Sqrt(combined.ColourDifficultyValue * difficulty_multiplier); - double rhythmRating = Math.Sqrt(combined.RhythmDifficultyValue * difficulty_multiplier); - double staminaRating = Math.Sqrt(combined.StaminaDifficultyValue * difficulty_multiplier); + double colourRating = combined.ColourDifficultyValue * difficulty_multiplier; + double rhythmRating = combined.RhythmDifficultyValue * difficulty_multiplier; + double staminaRating = combined.StaminaDifficultyValue * difficulty_multiplier; - double combinedRating = combined.DifficultyValue(); - double starRating = rescale(combinedRating * difficulty_multiplier); + double combinedRating = combined.DifficultyValue() * difficulty_multiplier; + double starRating = rescale(combinedRating * 1.4); + + // TODO: This is temporary measure as we don't detect abuse-type playstyles of converts within the current system. + if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) + { + starRating *= 0.80; + } HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); @@ -81,7 +87,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } /// - /// Applies a final re-scaling of the star rating to bring maps with recorded full combos below 9.5 stars. + /// Applies a final re-scaling of the star rating. /// /// The raw star rating value before re-scaling. private double rescale(double sr) From 8a17b509d91d6924ec79181c4b4c99f264d0694c Mon Sep 17 00:00:00 2001 From: Jay L Date: Sat, 16 Jul 2022 21:20:25 +1000 Subject: [PATCH 042/532] Increase SpeedBonus Cap to 600BPM --- .../Difficulty/Evaluators/StaminaEvaluator.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index 30918681f6..5906f39c33 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -16,13 +16,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// The interval between the current and previous note hit using the same key. private static double speedBonus(double interval) { - // Cap to 300bpm 1/4, 50ms note interval, 100ms key interval - // This is a temporary measure to prevent absurdly high speed mono convert maps being rated too high - // There is a plan to replace this with detecting mono that can be hit by special techniques, and this will - // be removed when that is implemented. + // Cap to 600bpm 1/4, 25ms note interval, 50ms key interval + // This is a measure to prevent absurdly high speed maps giving infinity/negative values. interval = Math.Max(interval, 100); - return 30 / interval; + return 60 / interval; } /// From 8beb5568b82108664ff3f97b9bee0808477f5bd8 Mon Sep 17 00:00:00 2001 From: vun Date: Sat, 16 Jul 2022 19:45:35 +0800 Subject: [PATCH 043/532] Fix speed bonus --- .../Difficulty/Evaluators/StaminaEvaluator.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index 5906f39c33..35f1d386a4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -17,10 +17,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators private static double speedBonus(double interval) { // Cap to 600bpm 1/4, 25ms note interval, 50ms key interval - // This is a measure to prevent absurdly high speed maps giving infinity/negative values. - interval = Math.Max(interval, 100); + // This is temporary measure to prevent mono abuses. Once that is properly addressed, interval will be + // capped at a very small value to avoid infinite/negative speed bonuses. + interval = Math.Max(interval, 50); - return 60 / interval; + return 30 / interval; } /// From a66fd87274ee4576cb3744f9040980d85a3600ba Mon Sep 17 00:00:00 2001 From: vun Date: Sat, 16 Jul 2022 19:48:29 +0800 Subject: [PATCH 044/532] Fix speed bonus comment --- .../Difficulty/Evaluators/StaminaEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index 35f1d386a4..f9f27de9c4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators private static double speedBonus(double interval) { // Cap to 600bpm 1/4, 25ms note interval, 50ms key interval - // This is temporary measure to prevent mono abuses. Once that is properly addressed, interval will be + // This a is temporary measure to prevent mono abuses. Once that is properly addressed, interval will be // capped at a very small value to avoid infinite/negative speed bonuses. interval = Math.Max(interval, 50); From e82e11ead59a298e6a7e507d271b8eec8b9c0535 Mon Sep 17 00:00:00 2001 From: Jay L Date: Sun, 17 Jul 2022 14:56:07 +1000 Subject: [PATCH 045/532] Fix SpeedBonus xml --- .idea/.idea.osu/.idea/discord.xml | 7 +++++++ .../Difficulty/Evaluators/StaminaEvaluator.cs | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .idea/.idea.osu/.idea/discord.xml diff --git a/.idea/.idea.osu/.idea/discord.xml b/.idea/.idea.osu/.idea/discord.xml new file mode 100644 index 0000000000..30bab2abb1 --- /dev/null +++ b/.idea/.idea.osu/.idea/discord.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index f9f27de9c4..954c1661dd 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators private static double speedBonus(double interval) { // Cap to 600bpm 1/4, 25ms note interval, 50ms key interval - // This a is temporary measure to prevent mono abuses. Once that is properly addressed, interval will be - // capped at a very small value to avoid infinite/negative speed bonuses. + // Interval will be capped at a very small value to avoid infinite/negative speed bonuses. + // TODO - This is a temporary measure as we need to implement methods of detecting playstyle-abuse of SpeedBonus. interval = Math.Max(interval, 50); return 30 / interval; From 77fa5674532cdd9c21e2e7697e86eeca8f14356a Mon Sep 17 00:00:00 2001 From: Jay L Date: Sun, 17 Jul 2022 14:56:25 +1000 Subject: [PATCH 046/532] Adjust tests --- .../TaikoDifficultyCalculatorTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 579b461624..425f72cadc 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -16,13 +16,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(1.9971301024093662d, 200, "diffcalc-test")] - [TestCase(1.9971301024093662d, 200, "diffcalc-test-strong")] + [TestCase(3.1098944660126882d, 200, "diffcalc-test")] + [TestCase(3.1098944660126882d, 200, "diffcalc-test-strong")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(3.1645810961313674d, 200, "diffcalc-test")] - [TestCase(3.1645810961313674d, 200, "diffcalc-test-strong")] + [TestCase(4.0974106752474251d, 200, "diffcalc-test")] + [TestCase(4.0974106752474251d, 200, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime()); From 9e299bb88b3d1938fb25ef18ec29aa49396d45bd Mon Sep 17 00:00:00 2001 From: Jay L Date: Sun, 17 Jul 2022 18:54:26 +1000 Subject: [PATCH 047/532] Delete Idea Project Notation --- .idea/.idea.osu/.idea/discord.xml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .idea/.idea.osu/.idea/discord.xml diff --git a/.idea/.idea.osu/.idea/discord.xml b/.idea/.idea.osu/.idea/discord.xml deleted file mode 100644 index 30bab2abb1..0000000000 --- a/.idea/.idea.osu/.idea/discord.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file From dcce14ae8fef76b19f632e12117b3f8ee705eb8b Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Sun, 17 Jul 2022 21:15:29 +0100 Subject: [PATCH 048/532] rename `NoConflictingModAcronyms` to `ModValidity`, add test for two-way mod incompatibility --- .../Visual/Gameplay/TestSceneModValidity.cs | 47 +++++++++++++++++++ .../TestSceneNoConflictingModAcronyms.cs | 27 ----------- 2 files changed, 47 insertions(+), 27 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs delete mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneNoConflictingModAcronyms.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs new file mode 100644 index 0000000000..b37b3dd93e --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Tests.Visual.Gameplay +{ + [HeadlessTest] + public class TestSceneModValidity : TestSceneAllRulesetPlayers + { + protected override void AddCheckSteps() + { + AddStep("Check all mod acronyms are unique", () => + { + var mods = Ruleset.Value.CreateInstance().AllMods; + + IEnumerable acronyms = mods.Select(m => m.Acronym); + + Assert.That(acronyms, Is.Unique); + }); + + AddStep("Check all mods are two-way incompatible", () => + { + var mods = Ruleset.Value.CreateInstance().AllMods; + + IEnumerable modInstances = mods.Select(mod => mod.CreateInstance()); + + foreach (var mod in modInstances) + { + var modIncompatibilities = mod.IncompatibleMods; + + foreach (var incompatibleModType in modIncompatibilities) + { + var incompatibleMod = (Mod)Activator.CreateInstance(incompatibleModType); + Assert.That(incompatibleMod?.IncompatibleMods.Contains(mod.GetType()) ?? false); + } + } + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneNoConflictingModAcronyms.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneNoConflictingModAcronyms.cs deleted file mode 100644 index b2ba3d99ad..0000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneNoConflictingModAcronyms.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Testing; - -namespace osu.Game.Tests.Visual.Gameplay -{ - [HeadlessTest] - public class TestSceneNoConflictingModAcronyms : TestSceneAllRulesetPlayers - { - protected override void AddCheckSteps() - { - AddStep("Check all mod acronyms are unique", () => - { - var mods = Ruleset.Value.CreateInstance().AllMods; - - IEnumerable acronyms = mods.Select(m => m.Acronym); - - Assert.That(acronyms, Is.Unique); - }); - } - } -} From 0c50931d2f072b36a1b0e4e4ff292e493dc549cd Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Sun, 17 Jul 2022 22:10:35 +0100 Subject: [PATCH 049/532] change method of finding `incompatibleMod` --- osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs index b37b3dd93e..4bb1b69853 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. #nullable disable -using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -37,8 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay foreach (var incompatibleModType in modIncompatibilities) { - var incompatibleMod = (Mod)Activator.CreateInstance(incompatibleModType); - Assert.That(incompatibleMod?.IncompatibleMods.Contains(mod.GetType()) ?? false); + var incompatibleMod = modInstances.First(m => m.GetType().IsInstanceOfType(incompatibleModType)); + Assert.That(incompatibleMod.IncompatibleMods.Contains(mod.GetType())); } } }); From 92513dc9367741de62159775f3db15c5810bf253 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Sun, 17 Jul 2022 22:49:09 +0100 Subject: [PATCH 050/532] reverse IsInstanceOfType logic --- osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs index 4bb1b69853..5ccfd87ca7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay foreach (var incompatibleModType in modIncompatibilities) { - var incompatibleMod = modInstances.First(m => m.GetType().IsInstanceOfType(incompatibleModType)); + var incompatibleMod = modInstances.First(m => incompatibleModType.IsInstanceOfType(m)); Assert.That(incompatibleMod.IncompatibleMods.Contains(mod.GetType())); } } From d17acd0f45cf54325f08981088991a52a7b5ea53 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Sun, 17 Jul 2022 23:16:27 +0100 Subject: [PATCH 051/532] add message to Assert.That --- osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs index 5ccfd87ca7..33e88d9ddb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs @@ -37,7 +37,10 @@ namespace osu.Game.Tests.Visual.Gameplay foreach (var incompatibleModType in modIncompatibilities) { var incompatibleMod = modInstances.First(m => incompatibleModType.IsInstanceOfType(m)); - Assert.That(incompatibleMod.IncompatibleMods.Contains(mod.GetType())); + Assert.That( + incompatibleMod.IncompatibleMods.Contains(mod.GetType()), + $"{mod} has {incompatibleMod} in it's incompatible mods, but {incompatibleMod} does not have {mod} in it's incompatible mods." + ); } } }); From d1c60b5741c7f28e1b795e35a1d5d0d268994d31 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Sun, 17 Jul 2022 23:53:35 +0100 Subject: [PATCH 052/532] correct assertion logic --- osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs index 33e88d9ddb..aa8dfaaa7e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var incompatibleMod = modInstances.First(m => incompatibleModType.IsInstanceOfType(m)); Assert.That( - incompatibleMod.IncompatibleMods.Contains(mod.GetType()), + incompatibleMod.IncompatibleMods.Any(m => m.IsInstanceOfType(mod)), $"{mod} has {incompatibleMod} in it's incompatible mods, but {incompatibleMod} does not have {mod} in it's incompatible mods." ); } From cb63ec282e135d19bb7330a518c186f24a8f4e5f Mon Sep 17 00:00:00 2001 From: Jay L Date: Wed, 20 Jul 2022 23:33:38 +1000 Subject: [PATCH 053/532] Partial Review changes --- .../Difficulty/Evaluators/ColourEvaluator.cs | 24 +++++++++++-------- .../Difficulty/Evaluators/StaminaEvaluator.cs | 2 +- .../Colour/Data/CoupledColourEncoding.cs | 4 ++-- .../Preprocessing/Colour/Data/MonoEncoding.cs | 1 - .../Preprocessing/TaikoDifficultyHitObject.cs | 8 +++---- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 1d857a1dbb..bda161bf63 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -10,14 +10,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { public class ColourEvaluator { - private static double sigmoid(double val, double center, double width) + /// + /// A sigmoid function. It gives a value between (middle - height/2) and (middle + height/2). + /// + /// The input value. + /// The center of the sigmoid, where the largest gradient occurs and value is equal to middle. + /// The radius of the sigmoid, outside of which values are near the minimum/maximum. + /// The middle of the sigmoid output. + /// The height of the sigmoid output. This will be equal to max value - min value. + public static double Sigmoid(double val, double center, double width, double middle, double height) { - return Math.Tanh(Math.E * -(val - center) / width); - } - - private static double sigmoid(double val, double center, double width, double middle, double height) - { - return sigmoid(val, center, width) * (height / 2) + middle; + double sigmoid = Math.Tanh(Math.E * -(val - center) / width); + return sigmoid * (height / 2) + middle; } /// @@ -27,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// public static double EvaluateDifficultyOf(MonoEncoding encoding, int i) { - return sigmoid(i, 2, 2, 0.5, 1); + return Sigmoid(i, 2, 2, 0.5, 1); } /// @@ -37,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// The index of the colour encoding within it's parent . public static double EvaluateDifficultyOf(ColourEncoding encoding, int i) { - return sigmoid(i, 2, 2, 0.5, 1); + return Sigmoid(i, 2, 2, 0.5, 1); } /// @@ -45,7 +49,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// public static double EvaluateDifficultyOf(CoupledColourEncoding encoding) { - return 1 - sigmoid(encoding.RepetitionInterval, 2, 2, 0.5, 1); + return 1 - Sigmoid(encoding.RepetitionInterval, 2, 2, 0.5, 1); } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs index 954c1661dd..49b3ae2e19 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// /// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the - /// maximum possible interval between two hits using the same key, by alternating 2 keys for each colour. + /// maximum possible interval between two hits using the same key, by alternating 2 keys for each colour. /// public static double EvaluateDifficultyOf(DifficultyHitObject current) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs index 188d1b686b..9d204225fc 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data public int RepetitionInterval { get; private set; } = max_repetition_interval + 1; /// - /// Returns true if other is considered a repetition of this encoding. This is true if other's first two payload - /// identical mono lengths. + /// Returns true if other is considered a repetition of this encoding. This is true if other's first two payloads + /// have identical mono lengths. /// private bool isRepetitionOf(CoupledColourEncoding other) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs index 9e60946bd1..f42f968657 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs @@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// /// List of s that are encoded within this . /// This is not declared as to avoid circular dependencies. - /// TODO: Review this, are circular dependencies within data-only classes are acceptable? /// public List EncodedData { get; private set; } = new List(); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index a0d2fc7797..6619a54a7a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { private readonly IReadOnlyList? monoDifficultyHitObjects; public readonly int MonoIndex; - private readonly IReadOnlyList noteObjects; + private readonly IReadOnlyList noteDifficultyHitObjects; public readonly int NoteIndex; /// @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing : base(hitObject, lastObject, clockRate, objects, index) { var currentHit = hitObject as Hit; - this.noteObjects = noteObjects; + noteDifficultyHitObjects = noteObjects; Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate); HitType? hitType = currentHit?.Type; @@ -120,8 +120,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public TaikoDifficultyHitObject? NextMono(int forwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex + (forwardsIndex + 1)); - public TaikoDifficultyHitObject? PreviousNote(int backwardsIndex) => noteObjects.ElementAtOrDefault(NoteIndex - (backwardsIndex + 1)); + public TaikoDifficultyHitObject? PreviousNote(int backwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex - (backwardsIndex + 1)); - public TaikoDifficultyHitObject? NextNote(int forwardsIndex) => noteObjects.ElementAtOrDefault(NoteIndex + (forwardsIndex + 1)); + public TaikoDifficultyHitObject? NextNote(int forwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex + (forwardsIndex + 1)); } } From 08dd9c79db55bcee648c2ae891c5969a63315ae5 Mon Sep 17 00:00:00 2001 From: Jay L Date: Thu, 21 Jul 2022 09:55:19 +1000 Subject: [PATCH 054/532] Fix Convert-related nerf This addresses recent player unsatisfaction with converts being underweighted. --- .../Difficulty/TaikoDifficultyCalculator.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index a70d3d9a38..3a230f7b91 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -64,10 +64,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double combinedRating = combined.DifficultyValue() * difficulty_multiplier; double starRating = rescale(combinedRating * 1.4); - // TODO: This is temporary measure as we don't detect abuse-type playstyles of converts within the current system. + // TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system. if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) { - starRating *= 0.80; + starRating *= 0.925; + // For maps with low colour variance and high stamina requirement, multiple inputs are more likely to be abused. + if (colourRating < 2 && staminaRating > 8) + starRating *= 0.80; } HitWindows hitWindows = new TaikoHitWindows(); From b7567f7db2f3714693de8244d53f5192a69e19ab Mon Sep 17 00:00:00 2001 From: Jay L Date: Thu, 21 Jul 2022 10:52:41 +1000 Subject: [PATCH 055/532] Share sigmoid, Fix Preprocessor XML --- .../Difficulty/Evaluators/ColourEvaluator.cs | 21 +++++-------------- .../TaikoColourDifficultyPreprocessor.cs | 16 +++++++------- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index bda161bf63..f677fe8b25 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -25,21 +25,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators } /// - /// Evaluate the difficulty of the first note of a . - /// The encoding to evaluate. - /// The index of the mono encoding within it's parent . + /// Evaluate the difficulty of the first note of a or a . + /// The index of either encoding within it's respective parent. /// - public static double EvaluateDifficultyOf(MonoEncoding encoding, int i) - { - return Sigmoid(i, 2, 2, 0.5, 1); - } - - /// - /// Evaluate the difficulty of the first note of a . - /// - /// The encoding to evaluate. - /// The index of the colour encoding within it's parent . - public static double EvaluateDifficultyOf(ColourEncoding encoding, int i) + public static double EvaluateDifficultyOf(int i) { return Sigmoid(i, 2, 2, 0.5, 1); } @@ -65,13 +54,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators for (int i = 0; i < encoding.Payload.Count; i++) { ColourEncoding colourEncoding = encoding.Payload[i]; - double colourEncodingDifficulty = EvaluateDifficultyOf(colourEncoding, i) * coupledEncodingDifficulty; + double colourEncodingDifficulty = EvaluateDifficultyOf(i) * coupledEncodingDifficulty; colourEncoding.Payload[0].EncodedData[0].Colour!.EvaluatedDifficulty += colourEncodingDifficulty; for (int j = 0; j < colourEncoding.Payload.Count; j++) { MonoEncoding monoEncoding = colourEncoding.Payload[j]; - monoEncoding.EncodedData[0].Colour!.EvaluatedDifficulty += EvaluateDifficultyOf(monoEncoding, j) * colourEncodingDifficulty * 0.5; + monoEncoding.EncodedData[0].Colour!.EvaluatedDifficulty += EvaluateDifficultyOf(j) * colourEncodingDifficulty * 0.5; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index a699422d2c..a3238efc65 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -16,9 +16,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour public class TaikoColourDifficultyPreprocessor { /// - /// Process and encode a list of s into a list of s, - /// assign the appropriate s to each , - /// and preevaluate colour difficulty of each . + /// Processes and encodes a list of s into a list of s, + /// assigning the appropriate s to each , + /// and pre-evaluating colour difficulty of each . /// public static List ProcessAndAssign(List hitObjects) { @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour }); }); - // Preevaluate and assign difficulty values + // Pre-evaluate and assign difficulty values ColourEvaluator.PreEvaluateDifficulties(coupledEncoding); }); @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour // This ignores all non-note objects, which may or may not be the desired behaviour TaikoDifficultyHitObject? previousObject = taikoObject.PreviousNote(0); - // If the colour changed, or if this is the first object in the run, create a new mono encoding + // If the colour changed or if this is the first object in the run, create a new mono encoding if ( previousObject == null || // First object in the list @@ -75,8 +75,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour continue; } - // If we're here, we're in the same encoding as the previous object, thus lastEncoded is not null. Add - // the current object to the encoded payload. + // If we're here, we're in the same encoding as the previous object, thus lastEncoded is not null. + // Add the current object to the encoded payload. lastEncoded!.EncodedData.Add(taikoObject); } @@ -159,7 +159,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour isCoupled = i < data.Count - 2 && data[i].IsRepetitionOf(data[i + 2]); } - // Skip over peeked data and add the rest to the payload + // Skip over viewed data and add the rest to the payload lastEncoded.Payload.Add(data[i]); lastEncoded.Payload.Add(data[i + 1]); i++; From 7917a60e3c18c1f9f9d9c54fc7f1ae811b6cc470 Mon Sep 17 00:00:00 2001 From: vun Date: Thu, 21 Jul 2022 15:45:03 +0800 Subject: [PATCH 056/532] Move TaikoDifficultyHitObject creation back to TaikoDifficultyCalculator --- .../TaikoDifficultyPreprocessor.cs | 26 +++---------------- .../Difficulty/TaikoDifficultyCalculator.cs | 16 +++++++++++- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs index fa8e6a8a26..bd703b7263 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing @@ -11,31 +10,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public class TaikoDifficultyPreprocessor { /// - /// Creates a list of s from a s. - /// This is placed here in a separate class to avoid having to know - /// too much implementation details of the preprocessing, and avoid - /// having circular dependencies with various preprocessing and evaluator classes. + /// Does preprocessing on a list of s. + /// TODO: Review this - this is currently only a one-step process, but will potentially be expanded in the future. /// - /// The beatmap from which the list of is created. - /// The rate at which the gameplay clock is run at. - public static List CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) + public static List Process(List difficultyHitObjects) { - List difficultyHitObjects = new List(); - List centreObjects = new List(); - List rimObjects = new List(); - List noteObjects = new List(); - - for (int i = 2; i < beatmap.HitObjects.Count; i++) - { - difficultyHitObjects.Add( - new TaikoDifficultyHitObject( - beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects, - centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count) - ); - } - TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); - return difficultyHitObjects; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 3a230f7b91..716a016b12 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -47,7 +47,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - return TaikoDifficultyPreprocessor.CreateDifficultyHitObjects(beatmap, clockRate); + List difficultyHitObjects = new List(); + List centreObjects = new List(); + List rimObjects = new List(); + List noteObjects = new List(); + + for (int i = 2; i < beatmap.HitObjects.Count; i++) + { + difficultyHitObjects.Add( + new TaikoDifficultyHitObject( + beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects, + centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count) + ); + } + + return TaikoDifficultyPreprocessor.Process(difficultyHitObjects); } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) From e4086b058bb3052a89c41c709bd6483b3fe15809 Mon Sep 17 00:00:00 2001 From: vun Date: Thu, 21 Jul 2022 19:15:22 +0800 Subject: [PATCH 057/532] Implement stateless colour evaluator and required encoding changes --- .../Difficulty/Evaluators/ColourEvaluator.cs | 49 +++++++------- .../Colour/Data/ColourEncoding.cs | 7 ++ .../Preprocessing/Colour/Data/MonoEncoding.cs | 7 ++ .../TaikoColourDifficultyPreprocessor.cs | 66 +++++++++++-------- .../Colour/TaikoDifficultyHitObjectColour.cs | 24 ++++--- .../Preprocessing/TaikoDifficultyHitObject.cs | 5 +- .../Difficulty/Skills/Colour.cs | 5 +- 7 files changed, 99 insertions(+), 64 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index f677fe8b25..30094dc869 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data; @@ -18,19 +19,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// The radius of the sigmoid, outside of which values are near the minimum/maximum. /// The middle of the sigmoid output. /// The height of the sigmoid output. This will be equal to max value - min value. - public static double Sigmoid(double val, double center, double width, double middle, double height) + private static double sigmoid(double val, double center, double width, double middle, double height) { double sigmoid = Math.Tanh(Math.E * -(val - center) / width); return sigmoid * (height / 2) + middle; } /// - /// Evaluate the difficulty of the first note of a or a . - /// The index of either encoding within it's respective parent. + /// Evaluate the difficulty of the first note of a . /// - public static double EvaluateDifficultyOf(int i) + public static double EvaluateDifficultyOf(MonoEncoding encoding) { - return Sigmoid(i, 2, 2, 0.5, 1); + return sigmoid(encoding.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(encoding.Parent!) * 0.5; + } + + /// + /// Evaluate the difficulty of the first note of a . + /// + public static double EvaluateDifficultyOf(ColourEncoding encoding) + { + return sigmoid(encoding.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(encoding.Parent!); } /// @@ -38,31 +46,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// public static double EvaluateDifficultyOf(CoupledColourEncoding encoding) { - return 1 - Sigmoid(encoding.RepetitionInterval, 2, 2, 0.5, 1); + return 2 * (1 - sigmoid(encoding.RepetitionInterval, 2, 2, 0.5, 1)); } - /// - /// Pre-evaluate and *assign* difficulty values of all hit objects encoded in a . - /// Difficulty values are assigned to of each - /// encoded within. - /// - public static void PreEvaluateDifficulties(CoupledColourEncoding encoding) + public static double EvaluateDifficultyOf(DifficultyHitObject hitObject) { - double coupledEncodingDifficulty = 2 * EvaluateDifficultyOf(encoding); - encoding.Payload[0].Payload[0].EncodedData[0].Colour!.EvaluatedDifficulty += coupledEncodingDifficulty; + TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour; + double difficulty = 0.0d; - for (int i = 0; i < encoding.Payload.Count; i++) - { - ColourEncoding colourEncoding = encoding.Payload[i]; - double colourEncodingDifficulty = EvaluateDifficultyOf(i) * coupledEncodingDifficulty; - colourEncoding.Payload[0].EncodedData[0].Colour!.EvaluatedDifficulty += colourEncodingDifficulty; + if (colour.MonoEncoding != null) // Difficulty for MonoEncoding + difficulty += EvaluateDifficultyOf(colour.MonoEncoding); + if (colour.ColourEncoding != null) // Difficulty for ColourEncoding + difficulty += EvaluateDifficultyOf(colour.ColourEncoding); + if (colour.CoupledColourEncoding != null) // Difficulty for CoupledColourEncoding + difficulty += EvaluateDifficultyOf(colour.CoupledColourEncoding); - for (int j = 0; j < colourEncoding.Payload.Count; j++) - { - MonoEncoding monoEncoding = colourEncoding.Payload[j]; - monoEncoding.EncodedData[0].Colour!.EvaluatedDifficulty += EvaluateDifficultyOf(j) * colourEncodingDifficulty * 0.5; - } - } + return difficulty; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs index cddf8816f5..23c5fa8b4a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs @@ -17,6 +17,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// public List Payload { get; private set; } = new List(); + public CoupledColourEncoding? Parent; + + /// + /// Index of this encoding within it's parent encoding + /// + public int Index; + /// /// Determine if this is a repetition of another . This /// is a strict comparison and is true if and only if the colour sequence is exactly the same. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs index f42f968657..abeba53e9a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs @@ -19,6 +19,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// public List EncodedData { get; private set; } = new List(); + public ColourEncoding? Parent; + + /// + /// Index of this encoding within it's parent encoding + /// + public int Index; + public int RunLength => EncodedData.Count; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index a3238efc65..d775246a2e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data; using osu.Game.Rulesets.Taiko.Objects; @@ -25,22 +24,36 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour List colours = new List(); List encodings = Encode(hitObjects); - // Assign colour to objects + // Assign indexing and encoding data to all relevant objects. Only the first note of each encoding type is + // assigned with the relevant encodings. encodings.ForEach(coupledEncoding => { - coupledEncoding.Payload.ForEach(encoding => - { - encoding.Payload.ForEach(mono => - { - mono.EncodedData.ForEach(hitObject => - { - hitObject.Colour = new TaikoDifficultyHitObjectColour(coupledEncoding); - }); - }); - }); + coupledEncoding.Payload[0].Payload[0].EncodedData[0].Colour.CoupledColourEncoding = coupledEncoding; - // Pre-evaluate and assign difficulty values - ColourEvaluator.PreEvaluateDifficulties(coupledEncoding); + // TODO: Review this - + // The outermost loop is kept a ForEach loop since it doesn't need index information, and we want to + // keep i and j for ColourEncoding's and MonoEncoding's index respectively, to keep it in line with + // documentation. + // If we want uniformity for the outermost loop, it can be switched to a for loop with h or something + // else as an index + // + // While parent and index should be part of the encoding process, they are assigned here instead due to + // this being a simple one location to assign them. + for (int i = 0; i < coupledEncoding.Payload.Count; ++i) + { + ColourEncoding colourEncoding = coupledEncoding.Payload[i]; + colourEncoding.Parent = coupledEncoding; + colourEncoding.Index = i; + colourEncoding.Payload[0].EncodedData[0].Colour.ColourEncoding = colourEncoding; + + for (int j = 0; j < colourEncoding.Payload.Count; ++j) + { + MonoEncoding monoEncoding = colourEncoding.Payload[j]; + monoEncoding.Parent = colourEncoding; + monoEncoding.Index = j; + monoEncoding.EncodedData[0].Colour.MonoEncoding = monoEncoding; + } + } }); return colours; @@ -67,7 +80,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour previousObject == null || // First object in the list (taikoObject.BaseObject as Hit)?.Type != (previousObject.BaseObject as Hit)?.Type ) - { lastEncoded = new MonoEncoding(); lastEncoded.EncodedData.Add(taikoObject); @@ -111,18 +123,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour return encoded; } - /// - /// Encodes a list of s into a list of s. - /// - public static List Encode(List data) - { - List firstPass = EncodeMono(data); - List secondPass = EncodeColour(firstPass); - List thirdPass = EncodeCoupledColour(secondPass); - - return thirdPass; - } - /// /// Encodes a list of s into a list of s. /// @@ -176,5 +176,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour return encoded; } + + /// + /// Encodes a list of s into a list of s. + /// + public static List Encode(List data) + { + List firstPass = EncodeMono(data); + List secondPass = EncodeColour(firstPass); + List thirdPass = EncodeCoupledColour(secondPass); + + return thirdPass; + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs index 9fc7a1dacb..6a6b427393 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs @@ -6,18 +6,26 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour { /// - /// Stores colour compression information for a . This is only present for the - /// first in a chunk. + /// Stores colour compression information for a . /// public class TaikoDifficultyHitObjectColour { - public CoupledColourEncoding Encoding { get; } + /// + /// encoding that encodes this note, only present if this is the first note within a + /// + /// + public MonoEncoding? MonoEncoding; - public double EvaluatedDifficulty = 0; + /// + /// encoding that encodes this note, only present if this is the first note within + /// a + /// + public ColourEncoding? ColourEncoding; - public TaikoDifficultyHitObjectColour(CoupledColourEncoding encoding) - { - Encoding = encoding; - } + /// + /// encoding that encodes this note, only present if this is the first note + /// within a + /// + public CoupledColourEncoding? CoupledColourEncoding; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 6619a54a7a..14fd67be33 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// by other skills in the future. /// This need to be writeable by TaikoDifficultyHitObjectColour so that it can assign potentially reused instances /// - public TaikoDifficultyHitObjectColour? Colour; + public TaikoDifficultyHitObjectColour Colour; /// /// Creates a new difficulty hit object. @@ -53,6 +53,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing List noteObjects, int index) : base(hitObject, lastObject, clockRate, objects, index) { + // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor + Colour = new TaikoDifficultyHitObjectColour(); + var currentHit = hitObject as Hit; noteDifficultyHitObjects = noteObjects; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 8f8f62d214..386135ea4d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -6,7 +6,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Evaluators; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { @@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double StrainValueOf(DifficultyHitObject current) { - double difficulty = ((TaikoDifficultyHitObject)current).Colour?.EvaluatedDifficulty ?? 0; - return difficulty; + return ColourEvaluator.EvaluateDifficultyOf(current); } } } From 4433f902ea4a20a6ec7bbcf70ee03ce1c26134a3 Mon Sep 17 00:00:00 2001 From: vun Date: Fri, 22 Jul 2022 10:49:53 +0800 Subject: [PATCH 058/532] Fix and add comments --- .../Preprocessing/Colour/Data/MonoEncoding.cs | 3 +++ .../Colour/TaikoColourDifficultyPreprocessor.cs | 3 --- .../Preprocessing/TaikoDifficultyHitObject.cs | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs index abeba53e9a..6f25eea51e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs @@ -26,6 +26,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// public int Index; + /// + /// How long the mono pattern encoded within is + /// public int RunLength => EncodedData.Count; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index d775246a2e..3772013e7a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -30,12 +30,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour { coupledEncoding.Payload[0].Payload[0].EncodedData[0].Colour.CoupledColourEncoding = coupledEncoding; - // TODO: Review this - // The outermost loop is kept a ForEach loop since it doesn't need index information, and we want to // keep i and j for ColourEncoding's and MonoEncoding's index respectively, to keep it in line with // documentation. - // If we want uniformity for the outermost loop, it can be switched to a for loop with h or something - // else as an index // // While parent and index should be part of the encoding process, they are assigned here instead due to // this being a simple one location to assign them. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 14fd67be33..fd9a225f6a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -17,9 +17,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// public class TaikoDifficultyHitObject : DifficultyHitObject { + /// + /// The list of all of the same colour as this in the beatmap. + /// private readonly IReadOnlyList? monoDifficultyHitObjects; + + /// + /// The index of this in . + /// public readonly int MonoIndex; + + /// + /// The list of all that is either a regular note or finisher in the beatmap + /// private readonly IReadOnlyList noteDifficultyHitObjects; + + /// + /// The index of this in . + /// public readonly int NoteIndex; /// From 6359c1a4fea5ec9790cbf276804a4f9a2233228d Mon Sep 17 00:00:00 2001 From: vun Date: Fri, 22 Jul 2022 16:31:19 +0800 Subject: [PATCH 059/532] Fix outdated comment --- .../Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs index 23c5fa8b4a..9415769dab 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs @@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// /// Determine if this is a repetition of another . This /// is a strict comparison and is true if and only if the colour sequence is exactly the same. - /// This does not require the s to have the same amount of s. /// public bool IsRepetitionOf(ColourEncoding other) { From 7d4593eb6ddbbc1d4a257d2bf8a3622f39a79ce0 Mon Sep 17 00:00:00 2001 From: vun Date: Fri, 22 Jul 2022 18:20:35 +0800 Subject: [PATCH 060/532] Fix comments --- .../Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs | 3 +++ .../Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs | 3 +++ .../Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs | 1 - 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs index 9415769dab..04066e7539 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs @@ -17,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// public List Payload { get; private set; } = new List(); + /// + /// The parent that contains this + /// public CoupledColourEncoding? Parent; /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs index 6f25eea51e..7eee8896ac 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs @@ -19,6 +19,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// public List EncodedData { get; private set; } = new List(); + /// + /// The parent that contains this + /// public ColourEncoding? Parent; /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs index bd703b7263..c5ee8de809 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs @@ -11,7 +11,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { /// /// Does preprocessing on a list of s. - /// TODO: Review this - this is currently only a one-step process, but will potentially be expanded in the future. /// public static List Process(List difficultyHitObjects) { From fc08d77090b6cdd0de9dca93dfe2c3bfb2b85647 Mon Sep 17 00:00:00 2001 From: vun Date: Fri, 22 Jul 2022 18:31:59 +0800 Subject: [PATCH 061/532] Remove review-specific comment --- .../Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index 3772013e7a..517e240682 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -33,9 +33,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour // The outermost loop is kept a ForEach loop since it doesn't need index information, and we want to // keep i and j for ColourEncoding's and MonoEncoding's index respectively, to keep it in line with // documentation. - // - // While parent and index should be part of the encoding process, they are assigned here instead due to - // this being a simple one location to assign them. for (int i = 0; i < coupledEncoding.Payload.Count; ++i) { ColourEncoding colourEncoding = coupledEncoding.Payload[i]; From 0a0f3c93ddf5574feb583b036808945f2a7be073 Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Sun, 24 Jul 2022 20:55:13 +0200 Subject: [PATCH 062/532] Rename OkResult, rephrase "strong bonus" --- osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs | 2 +- osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs index 90ba7e276c..6d469bd1d7 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Taiko.Judgements /// /// The to grant when the player has hit more than half of swell ticks. /// - public virtual HitResult OkResult => HitResult.Ok; + public virtual HitResult PartialCompletionResult => HitResult.Ok; protected override double HealthIncreaseFor(HitResult result) { diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs index ec4ad18080..efb67825db 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override HitResult MaxResult => HitResult.LargeBonus; - public override HitResult OkResult => HitResult.SmallBonus; + public override HitResult PartialCompletionResult => HitResult.SmallBonus; } private class ClassicSwell : Swell diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index cb0f59a9fb..a90e9ce676 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -225,7 +225,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => { var swellJudgement = (TaikoSwellJudgement)r.Judgement; - r.Type = numHits > HitObject.RequiredHits / 2 ? swellJudgement.OkResult : swellJudgement.MinResult; + r.Type = numHits > HitObject.RequiredHits / 2 ? swellJudgement.PartialCompletionResult : swellJudgement.MinResult; }); } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 223e268d7f..eb0706f131 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -200,7 +200,7 @@ namespace osu.Game.Rulesets.Taiko return "drum tick"; case HitResult.SmallBonus: - return "strong bonus"; + return "bonus"; } return base.GetDisplayNameForHitResult(result); From 8105d4854a53c48617ca7638a989a9442174e69a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jul 2022 13:30:38 +0900 Subject: [PATCH 063/532] Fix beatmap carousel not maintaining selection if currently selected beatmap is updated --- osu.Game/Screens/Select/BeatmapCarousel.cs | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 75caa3c9a3..c6f2b61049 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -265,6 +265,38 @@ namespace osu.Game.Screens.Select foreach (int i in changes.InsertedIndices) UpdateBeatmapSet(sender[i].Detach()); + + if (changes.DeletedIndices.Length > 0) + { + // To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions. + // When an update occurs, the previous beatmap set is either soft or hard deleted. + // Check if the current selection was potentially deleted by re-querying its validity. + bool selectedSetMarkedDeleted = realm.Run(r => r.Find(SelectedBeatmapSet.ID))?.DeletePending != false; + + if (selectedSetMarkedDeleted) + { + // If it is no longer valid, make the bold assumption that an updated version will be available in the modified indices. + // This relies on the full update operation being in a single transaction, so please don't change that. + foreach (int i in changes.NewModifiedIndices) + { + var beatmapSetInfo = sender[i]; + + foreach (var beatmapInfo in beatmapSetInfo.Beatmaps) + { + // Best effort matching. We can't use ID because in the update flow a new version will get its own GUID. + bool selectionMatches = + ((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata) + && beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName; + + if (selectionMatches) + { + SelectBeatmap(beatmapInfo); + break; + } + } + } + } + } } private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) From 24d75612e222fe3e055d1f5fabf623727dc81727 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jul 2022 14:18:53 +0900 Subject: [PATCH 064/532] Always attempt to follow selection, even if difficulty name / metadata change --- osu.Game/Screens/Select/BeatmapCarousel.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index c6f2b61049..0430c15a1d 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -283,18 +283,21 @@ namespace osu.Game.Screens.Select foreach (var beatmapInfo in beatmapSetInfo.Beatmaps) { - // Best effort matching. We can't use ID because in the update flow a new version will get its own GUID. - bool selectionMatches = - ((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata) - && beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName; + if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata)) + continue; - if (selectionMatches) + // Best effort matching. We can't use ID because in the update flow a new version will get its own GUID. + if (beatmapInfo.DifficultyName == SelectedBeatmapInfo.DifficultyName) { SelectBeatmap(beatmapInfo); - break; + return; } } } + + // If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed. + // Let's attempt to follow set-level selection anyway. + SelectBeatmap(sender[changes.NewModifiedIndices.First()].Beatmaps.First()); } } } From e6a3659581e62b9c181d1c0ffe4f82a8988a8d05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jul 2022 14:23:47 +0900 Subject: [PATCH 065/532] Guard against `NewModifiedIndices` being empty --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 0430c15a1d..e9419e7156 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -273,7 +273,7 @@ namespace osu.Game.Screens.Select // Check if the current selection was potentially deleted by re-querying its validity. bool selectedSetMarkedDeleted = realm.Run(r => r.Find(SelectedBeatmapSet.ID))?.DeletePending != false; - if (selectedSetMarkedDeleted) + if (selectedSetMarkedDeleted && changes.NewModifiedIndices.Any()) { // If it is no longer valid, make the bold assumption that an updated version will be available in the modified indices. // This relies on the full update operation being in a single transaction, so please don't change that. From e6fad601cc2106e983d13ba9a2383f8500b917ba Mon Sep 17 00:00:00 2001 From: Micahel Kelly Date: Wed, 27 Jul 2022 19:15:43 +1000 Subject: [PATCH 066/532] Adds delete difficulty option to editor --- osu.Game/Screens/Edit/Editor.cs | 18 ++++++++++ .../Edit/PromptForDifficultyDeleteDialog.cs | 33 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3e3940c5ba..5cee634fd8 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -849,6 +849,20 @@ namespace osu.Game.Screens.Edit new LegacyBeatmapExporter(storage).Export(Beatmap.Value.BeatmapSetInfo); } + private void deleteDifficulty() + { + dialogOverlay?.Push(new PromptForDifficultyDeleteDialog(confirmDifficultyDelete, () => { })); + } + + private void confirmDifficultyDelete() + { + var current = playableBeatmap.BeatmapInfo; + if (current is null) return; + + beatmapManager.Hide(current); + this.Exit(); + } + private void updateLastSavedHash() { lastSavedHash = changeHandler?.CurrentStateHash; @@ -869,6 +883,10 @@ namespace osu.Game.Screens.Edit fileMenuItems.Add(createDifficultyCreationMenu()); fileMenuItems.Add(createDifficultySwitchMenu()); + fileMenuItems.Add(new EditorMenuItemSpacer()); + + fileMenuItems.Add(new EditorMenuItem("Delete difficulty", MenuItemType.Standard, deleteDifficulty)); + fileMenuItems.Add(new EditorMenuItemSpacer()); fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)); return fileMenuItems; diff --git a/osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs b/osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs new file mode 100644 index 0000000000..abf66d7a5b --- /dev/null +++ b/osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Sprites; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Edit +{ + public class PromptForDifficultyDeleteDialog : PopupDialog + { + public PromptForDifficultyDeleteDialog(Action delete, Action cancel) + { + HeaderText = "Are you sure you want to delete this difficulty?"; + + Icon = FontAwesome.Regular.Save; + + Buttons = new PopupDialogButton[] + { + new PopupDialogDangerousButton + { + Text = @"Yes delete this difficulty!", + Action = delete + }, + new PopupDialogCancelButton + { + Text = @"Oops, continue editing", + Action = cancel + }, + }; + } + } +} From 3febd6d6443a75ea3e6d9ff29c390301f0a03872 Mon Sep 17 00:00:00 2001 From: Micahel Kelly Date: Wed, 27 Jul 2022 19:23:55 +1000 Subject: [PATCH 067/532] Dialogue touchups --- osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs b/osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs index abf66d7a5b..a8f8afd47c 100644 --- a/osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs +++ b/osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs @@ -13,13 +13,13 @@ namespace osu.Game.Screens.Edit { HeaderText = "Are you sure you want to delete this difficulty?"; - Icon = FontAwesome.Regular.Save; + Icon = FontAwesome.Regular.TrashAlt; Buttons = new PopupDialogButton[] { new PopupDialogDangerousButton { - Text = @"Yes delete this difficulty!", + Text = @"Yes, delete this difficulty!", Action = delete }, new PopupDialogCancelButton From dd315ff97b3d0f6c96fcd75796738f9e909e88f1 Mon Sep 17 00:00:00 2001 From: Micahel Kelly Date: Wed, 27 Jul 2022 21:21:16 +1000 Subject: [PATCH 068/532] Adds hard-delete for a beatmap diff --- osu.Game/Beatmaps/BeatmapManager.cs | 25 ++++++++++++++++++ osu.Game/Screens/Edit/Editor.cs | 26 ++++++++++++++++--- .../Edit/PromptForDifficultyDeleteDialog.cs | 9 +++++-- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index debe4c6829..6db038355f 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -358,6 +358,31 @@ namespace osu.Game.Beatmaps }); } + /// + /// Hard-Delete a beatmap difficulty locally. + /// + /// is generally preferred as it keeps the local beatmap set in sync with the server. + /// The beatmap difficulty to hide. + public void DeleteLocal(BeatmapInfo beatmapInfo) + { + Realm.Run(r => + { + using (var transaction = r.BeginWrite()) + { + if (!beatmapInfo.IsManaged) + beatmapInfo = r.Find(beatmapInfo.ID); + + var setInfo = beatmapInfo.BeatmapSet!; + DeleteFile(setInfo, beatmapInfo.File!); + setInfo.Beatmaps.Remove(beatmapInfo); + + var liveSetInfo = r.Find(setInfo.ID); + setInfo.CopyChangesToRealm(liveSetInfo); + transaction.Commit(); + } + }); + } + /// /// Delete videos from a list of beatmaps. /// This will post notifications tracking progress. diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 5cee634fd8..129e71ea30 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -851,7 +851,16 @@ namespace osu.Game.Screens.Edit private void deleteDifficulty() { - dialogOverlay?.Push(new PromptForDifficultyDeleteDialog(confirmDifficultyDelete, () => { })); + dialogOverlay?.Push(new PromptForDifficultyDeleteDialog(confirmDifficultyHide, confirmDifficultyDelete, () => { })); + } + + private void confirmDifficultyHide() + { + var current = playableBeatmap.BeatmapInfo; + if (current is null) return; + + beatmapManager.Hide(current); + switchBeatmapOrExit(current.BeatmapSet); } private void confirmDifficultyDelete() @@ -859,8 +868,19 @@ namespace osu.Game.Screens.Edit var current = playableBeatmap.BeatmapInfo; if (current is null) return; - beatmapManager.Hide(current); - this.Exit(); + beatmapManager.DeleteLocal(current); + switchBeatmapOrExit(current.BeatmapSet); + } + + private void switchBeatmapOrExit([CanBeNull] BeatmapSetInfo setInfo) + { + if (setInfo is null || setInfo.Beatmaps.Count() <= 1) + this.Exit(); + var nextBeatmap = setInfo!.Beatmaps.First(); + + // Force a refresh of the beatmap (and beatmap set) so the `Change difficulty` list is also updated. + Beatmap.Value = beatmapManager.GetWorkingBeatmap(nextBeatmap, true); + SwitchToDifficulty(Beatmap.Value.BeatmapInfo); } private void updateLastSavedHash() diff --git a/osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs b/osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs index a8f8afd47c..dcea9f1210 100644 --- a/osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs +++ b/osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs @@ -9,7 +9,7 @@ namespace osu.Game.Screens.Edit { public class PromptForDifficultyDeleteDialog : PopupDialog { - public PromptForDifficultyDeleteDialog(Action delete, Action cancel) + public PromptForDifficultyDeleteDialog(Action hide, Action delete, Action cancel) { HeaderText = "Are you sure you want to delete this difficulty?"; @@ -17,9 +17,14 @@ namespace osu.Game.Screens.Edit Buttons = new PopupDialogButton[] { + new PopupDialogOkButton + { + Text = @"Hide this difficulty instead (recommended)", + Action = hide + }, new PopupDialogDangerousButton { - Text = @"Yes, delete this difficulty!", + Text = @"Yes, DELETE this difficulty!", Action = delete }, new PopupDialogCancelButton From 013cf7a80aaca9da466063fb969b328d21e32022 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Aug 2022 00:21:13 +0900 Subject: [PATCH 069/532] Fix `DateAdded` not being set to a sane value when creating a new beatmap in the editor --- osu.Game/Beatmaps/BeatmapManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index e3d2832108..8ce4e4c2fb 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -94,6 +94,7 @@ namespace osu.Game.Beatmaps var beatmapSet = new BeatmapSetInfo { + DateAdded = DateTimeOffset.UtcNow, Beatmaps = { new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata) From d7a06abcab0f697057a4c9d2859faaa3aec95349 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Aug 2022 00:21:28 +0900 Subject: [PATCH 070/532] Add `BeatmapInfo.LastUpdate` to track the time of local changes --- osu.Game/Beatmaps/BeatmapInfo.cs | 8 ++++++++ osu.Game/Beatmaps/BeatmapManager.cs | 1 + osu.Game/Database/RealmAccess.cs | 3 ++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index c53c2f67dd..c0d72fdf8c 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -102,6 +102,14 @@ namespace osu.Game.Beatmaps public string OnlineMD5Hash { get; set; } = string.Empty; + /// + /// The last time of a local modification (via the editor). + /// + public DateTimeOffset? LastUpdated { get; set; } + + /// + /// The last time online metadata was applied to this beatmap. + /// public DateTimeOffset? LastOnlineUpdate { get; set; } /// diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 8ce4e4c2fb..b457fcb387 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -314,6 +314,7 @@ namespace osu.Game.Beatmaps beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.Hash = stream.ComputeSHA2Hash(); beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; + beatmapInfo.LastUpdated = DateTimeOffset.Now; if (setInfo.Beatmaps.All(b => b.Status == BeatmapOnlineStatus.LocallyModified)) setInfo.Status = BeatmapOnlineStatus.LocallyModified; diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 5f0ec67c71..c073415547 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -67,8 +67,9 @@ namespace osu.Game.Database /// 20 2022-07-21 Added LastAppliedDifficultyVersion to RulesetInfo, changed default value of BeatmapInfo.StarRating to -1. /// 21 2022-07-27 Migrate collections to realm (BeatmapCollection). /// 22 2022-07-31 Added ModPreset. + /// 23 2022-08-01 Added LastUpdated to BeatmapInfo. /// - private const int schema_version = 22; + private const int schema_version = 23; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. From efa3c3b75716f3355e9187f23a497267d2680f55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Aug 2022 16:35:15 +0900 Subject: [PATCH 071/532] Fix multiple cases of mutations at editor setup screen not triggering a state save --- osu.Game/Screens/Edit/EditorBeatmap.cs | 3 +++ osu.Game/Screens/Edit/Setup/DesignSection.cs | 2 ++ osu.Game/Screens/Edit/Setup/MetadataSection.cs | 2 ++ 3 files changed, 7 insertions(+) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 96425e8bc8..afbb5cc018 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -78,7 +78,10 @@ namespace osu.Game.Screens.Edit this.beatmapInfo = beatmapInfo ?? playableBeatmap.BeatmapInfo; if (beatmapSkin is Skin skin) + { BeatmapSkin = new EditorBeatmapSkin(skin); + BeatmapSkin.BeatmapSkinChanged += SaveState; + } beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapProcessor(PlayableBeatmap); diff --git a/osu.Game/Screens/Edit/Setup/DesignSection.cs b/osu.Game/Screens/Edit/Setup/DesignSection.cs index 40bbfeaf7d..5cec730440 100644 --- a/osu.Game/Screens/Edit/Setup/DesignSection.cs +++ b/osu.Game/Screens/Edit/Setup/DesignSection.cs @@ -126,6 +126,8 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning.Current.Value; Beatmap.BeatmapInfo.LetterboxInBreaks = letterboxDuringBreaks.Current.Value; Beatmap.BeatmapInfo.SamplesMatchPlaybackRate = samplesMatchPlaybackRate.Current.Value; + + Beatmap.SaveState(); } } } diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 854dec2001..33cc88da0a 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -117,6 +117,8 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.BeatmapInfo.DifficultyName = difficultyTextBox.Current.Value; Beatmap.Metadata.Source = sourceTextBox.Current.Value; Beatmap.Metadata.Tags = tagsTextBox.Current.Value; + + Beatmap.SaveState(); } } } From de186f67e0880aabc8cfb29e2bf019a9e8bd14db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Aug 2022 22:02:30 +0900 Subject: [PATCH 072/532] Limit metadata updates to once per frame --- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 33cc88da0a..928e5bc3b6 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Edit.Setup target.Current.Value = value; updateReadOnlyState(); - updateMetadata(); + Scheduler.AddOnce(updateMetadata); } private void updateReadOnlyState() @@ -102,7 +102,7 @@ namespace osu.Game.Screens.Edit.Setup // for now, update on commit rather than making BeatmapMetadata bindables. // after switching database engines we can reconsider if switching to bindables is a good direction. - updateMetadata(); + Scheduler.AddOnce(updateMetadata); } private void updateMetadata() From e0426836c19391139a2976ea364ed0122c94aa77 Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Fri, 5 Aug 2022 16:30:07 +0200 Subject: [PATCH 073/532] Make swells and drumrolls optional by default --- .../Mods/TestSceneTaikoModClassic.cs | 80 ---------- .../Mods/TestSceneTaikoModPerfect.cs | 4 +- .../TestSceneTaikoSuddenDeath.cs | 4 +- .../Judgements/TaikoDrumRollJudgement.cs | 13 +- .../Judgements/TaikoDrumRollTickJudgement.cs | 14 +- .../Judgements/TaikoSwellJudgement.cs | 7 +- .../Mods/TaikoModClassic.cs | 151 +----------------- .../Objects/Drawables/DrawableDrumRoll.cs | 13 +- .../Objects/Drawables/DrawableSwell.cs | 10 +- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 33 +--- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 3 - 11 files changed, 24 insertions(+), 308 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModClassic.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModClassic.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModClassic.cs deleted file mode 100644 index 9028f411ce..0000000000 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModClassic.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using NUnit.Framework; -using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Taiko.Beatmaps; -using osu.Game.Rulesets.Taiko.Mods; -using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.Replays; - -namespace osu.Game.Rulesets.Taiko.Tests.Mods -{ - public class TestSceneTaikoModClassic : TaikoModTestScene - { - [Test] - public void TestHittingDrumRollsIsOptional() => CreateModTest(new ModTestData - { - Mod = new TaikoModClassic(), - Autoplay = false, - Beatmap = new TaikoBeatmap - { - BeatmapInfo = { Ruleset = CreatePlayerRuleset().RulesetInfo }, - HitObjects = new List - { - new Hit - { - StartTime = 1000, - Type = HitType.Centre - }, - new DrumRoll - { - StartTime = 3000, - EndTime = 6000 - } - } - }, - ReplayFrames = new List - { - new TaikoReplayFrame(1000, TaikoAction.LeftCentre), - new TaikoReplayFrame(1001) - }, - PassCondition = () => Player.ScoreProcessor.HasCompleted.Value - && Player.ScoreProcessor.Combo.Value == 1 - && Player.ScoreProcessor.Accuracy.Value == 1 - }); - - [Test] - public void TestHittingSwellsIsOptional() => CreateModTest(new ModTestData - { - Mod = new TaikoModClassic(), - Autoplay = false, - Beatmap = new TaikoBeatmap - { - BeatmapInfo = { Ruleset = CreatePlayerRuleset().RulesetInfo }, - HitObjects = new List - { - new Hit - { - StartTime = 1000, - Type = HitType.Centre - }, - new Swell - { - StartTime = 3000, - EndTime = 6000 - } - } - }, - ReplayFrames = new List - { - new TaikoReplayFrame(1000, TaikoAction.LeftCentre), - new TaikoReplayFrame(1001) - }, - PassCondition = () => Player.ScoreProcessor.HasCompleted.Value - && Player.ScoreProcessor.Combo.Value == 1 - && Player.ScoreProcessor.Accuracy.Value == 1 - }); - } -} diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs index a83cc16413..92503a9f03 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs @@ -25,11 +25,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods [TestCase(false)] [TestCase(true)] - public void TestDrumRoll(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new DrumRoll { StartTime = 1000, EndTime = 3000 }), shouldMiss); + public void TestDrumRoll(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new DrumRoll { StartTime = 1000, EndTime = 3000 }, false), shouldMiss); [TestCase(false)] [TestCase(true)] - public void TestSwell(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Swell { StartTime = 1000, EndTime = 3000 }), shouldMiss); + public void TestSwell(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Swell { StartTime = 1000, EndTime = 3000 }, false), shouldMiss); private class TestTaikoRuleset : TaikoRuleset { diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index cdfab4a215..2169ac5581 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests }; [Test] - public void TestSpinnerDoesFail() + public void TestSwellDoesNotFail() { bool judged = false; AddStep("Setup judgements", () => @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Player.ScoreProcessor.NewJudgement += _ => judged = true; }); AddUntilStep("swell judged", () => judged); - AddAssert("failed", () => Player.GameplayState.HasFailed); + AddAssert("not failed", () => !Player.GameplayState.HasFailed); } } } diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs index be128d85b5..f7f923e76e 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs @@ -9,17 +9,8 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoDrumRollJudgement : TaikoJudgement { - protected override double HealthIncreaseFor(HitResult result) - { - // Drum rolls can be ignored with no health penalty - switch (result) - { - case HitResult.Miss: - return 0; + public override HitResult MaxResult => HitResult.IgnoreHit; - default: - return base.HealthIncreaseFor(result); - } - } + protected override double HealthIncreaseFor(HitResult result) => 0; } } diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs index 5f2587a5d5..de56c76f56 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs @@ -9,18 +9,8 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoDrumRollTickJudgement : TaikoJudgement { - public override HitResult MaxResult => HitResult.SmallTickHit; + public override HitResult MaxResult => HitResult.SmallBonus; - protected override double HealthIncreaseFor(HitResult result) - { - switch (result) - { - case HitResult.SmallTickHit: - return 0.15; - - default: - return 0; - } - } + protected override double HealthIncreaseFor(HitResult result) => 0; } } diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs index 6d469bd1d7..146621997d 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs @@ -9,16 +9,13 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoSwellJudgement : TaikoJudgement { - /// - /// The to grant when the player has hit more than half of swell ticks. - /// - public virtual HitResult PartialCompletionResult => HitResult.Ok; + public override HitResult MaxResult => HitResult.LargeBonus; protected override double HealthIncreaseFor(HitResult result) { switch (result) { - case HitResult.Miss: + case HitResult.IgnoreMiss: return -0.65; default: diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs index 28124d16e1..f7fdd447d6 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs @@ -1,170 +1,27 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; -using System.Collections.Generic; using System.Diagnostics; -using System.Threading; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Beatmaps; -using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset, IUpdatableByPlayfield, IApplicableAfterBeatmapConversion + public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset, IUpdatableByPlayfield { private DrawableTaikoRuleset? drawableTaikoRuleset; public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - var playfield = (TaikoPlayfield)drawableRuleset.Playfield; - playfield.ClassicHitTargetPosition.Value = true; - drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; drawableTaikoRuleset.LockPlayfieldAspect.Value = false; - drawableTaikoRuleset.Playfield.RegisterPool(5); - drawableTaikoRuleset.Playfield.RegisterPool(100); - drawableTaikoRuleset.Playfield.RegisterPool(5); + var playfield = (TaikoPlayfield)drawableRuleset.Playfield; + playfield.ClassicHitTargetPosition.Value = true; } - public void ApplyToBeatmap(IBeatmap beatmap) - { - var taikoBeatmap = (TaikoBeatmap)beatmap; - - if (taikoBeatmap.HitObjects.Count == 0) return; - - var hitObjects = taikoBeatmap.HitObjects.Select(ho => - { - switch (ho) - { - case DrumRoll drumRoll: - return new ClassicDrumRoll(drumRoll); - - case Swell swell: - return new ClassicSwell(swell); - - default: - return ho; - } - }).ToList(); - - taikoBeatmap.HitObjects = hitObjects; - } - - #region Classic drum roll - - private class TaikoClassicDrumRollJudgement : TaikoDrumRollJudgement - { - public override HitResult MaxResult => HitResult.IgnoreHit; - } - - private class ClassicDrumRoll : DrumRoll - { - public ClassicDrumRoll(DrumRoll original) - { - StartTime = original.StartTime; - Samples = original.Samples; - EndTime = original.EndTime; - Duration = original.Duration; - TickRate = original.TickRate; - RequiredGoodHits = original.RequiredGoodHits; - RequiredGreatHits = original.RequiredGreatHits; - } - - public override Judgement CreateJudgement() => new TaikoClassicDrumRollJudgement(); - - protected override List CreateTicks(CancellationToken cancellationToken) - { - List oldTicks = base.CreateTicks(cancellationToken); - - List newTicks = oldTicks.Select(oldTick => - { - if (oldTick is DrumRollTick drumRollTick) - { - return new ClassicDrumRollTick(drumRollTick); - } - - return oldTick; - }).ToList(); - - return newTicks; - } - } - - private class TaikoClassicDrumRollTickJudgement : TaikoDrumRollTickJudgement - { - public override HitResult MaxResult => HitResult.SmallBonus; - } - - private class ClassicDrumRollTick : DrumRollTick - { - public override Judgement CreateJudgement() => new TaikoClassicDrumRollTickJudgement(); - - public ClassicDrumRollTick(DrumRollTick original) - { - StartTime = original.StartTime; - Samples = original.Samples; - FirstTick = original.FirstTick; - TickSpacing = original.TickSpacing; - } - } - - private class ClassicDrawableDrumRoll : DrawableDrumRoll - { - public override bool DisplayResult => false; - - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - if (userTriggered) - return; - - if (timeOffset < 0) - return; - - ApplyResult(r => r.Type = HitResult.IgnoreHit); - } - } - - #endregion - - #region Classic swell - - private class TaikoClassicSwellJudgement : TaikoSwellJudgement - { - public override HitResult MaxResult => HitResult.LargeBonus; - - public override HitResult PartialCompletionResult => HitResult.SmallBonus; - } - - private class ClassicSwell : Swell - { - public ClassicSwell(Swell original) - { - StartTime = original.StartTime; - Samples = original.Samples; - EndTime = original.EndTime; - Duration = original.Duration; - RequiredHits = original.RequiredHits; - } - - public override Judgement CreateJudgement() => new TaikoClassicSwellJudgement(); - } - - private class ClassicDrawableSwell : DrawableSwell - { - public override bool DisplayResult => false; - } - - #endregion - public void Update(Playfield playfield) { Debug.Assert(drawableTaikoRuleset != null); @@ -172,8 +29,6 @@ namespace osu.Game.Rulesets.Taiko.Mods // Classic taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened. const float scroll_rate = 10; - Debug.Assert(drawableTaikoRuleset != null); - // Since the time range will depend on a positional value, it is referenced to the x480 pixel space. float ratio = drawableTaikoRuleset.DrawHeight / 480; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 04ed6d0b87..418c4673e2 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -4,7 +4,6 @@ #nullable disable using System; -using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Utils; @@ -16,7 +15,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; using osuTK; @@ -40,6 +38,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Color4 colourIdle; private Color4 colourEngaged; + public override bool DisplayResult => false; + public DrawableDrumRoll() : this(null) { @@ -140,14 +140,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (timeOffset < 0) return; - int countHit = NestedHitObjects.Count(o => o.IsHit); - - if (countHit >= HitObject.RequiredGoodHits) - { - ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok); - } - else - ApplyResult(r => r.Type = r.Judgement.MinResult); + ApplyResult(r => r.Type = r.Judgement.MinResult); } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index a90e9ce676..84edb30890 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -16,7 +16,7 @@ using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Taiko.Judgements; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; @@ -39,6 +39,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; + public override bool DisplayResult => false; + public DrawableSwell() : this(null) { @@ -222,11 +224,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables tick.TriggerResult(false); } - ApplyResult(r => - { - var swellJudgement = (TaikoSwellJudgement)r.Judgement; - r.Type = numHits > HitObject.RequiredHits / 2 ? swellJudgement.PartialCompletionResult : swellJudgement.MinResult; - }); + ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.SmallBonus : r.Judgement.MinResult); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 21c7f13fec..db752c6691 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -4,8 +4,6 @@ #nullable disable using osu.Game.Rulesets.Objects.Types; -using System; -using System.Collections.Generic; using System.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -43,24 +41,12 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public int TickRate = 1; - /// - /// Number of drum roll ticks required for a "Good" hit. - /// - public double RequiredGoodHits { get; protected set; } - - /// - /// Number of drum roll ticks required for a "Great" hit. - /// - public double RequiredGreatHits { get; protected set; } - /// /// The length (in milliseconds) between ticks of this drumroll. /// Half of this value is the hit window of the ticks. /// private double tickSpacing = 100; - private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -71,28 +57,19 @@ namespace osu.Game.Rulesets.Taiko.Objects Velocity = scoringDistance / timingPoint.BeatLength; tickSpacing = timingPoint.BeatLength / TickRate; - overallDifficulty = difficulty.OverallDifficulty; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - foreach (TaikoHitObject tick in CreateTicks(cancellationToken)) - { - AddNested(tick); - } - - RequiredGoodHits = NestedHitObjects.Count * Math.Min(0.15, 0.05 + 0.10 / 6 * overallDifficulty); - RequiredGreatHits = NestedHitObjects.Count * Math.Min(0.30, 0.10 + 0.20 / 6 * overallDifficulty); + createTicks(cancellationToken); base.CreateNestedHitObjects(cancellationToken); } - protected virtual List CreateTicks(CancellationToken cancellationToken) + private void createTicks(CancellationToken cancellationToken) { - List ticks = new List(); - if (tickSpacing == 0) - return ticks; + return; bool first = true; @@ -100,7 +77,7 @@ namespace osu.Game.Rulesets.Taiko.Objects { cancellationToken.ThrowIfCancellationRequested(); - ticks.Add(new DrumRollTick + AddNested(new DrumRollTick { FirstTick = first, TickSpacing = tickSpacing, @@ -110,8 +87,6 @@ namespace osu.Game.Rulesets.Taiko.Objects first = false; } - - return ticks; } public override Judgement CreateJudgement() => new TaikoDrumRollJudgement(); diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index eb0706f131..499bb8cf1d 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -196,9 +196,6 @@ namespace osu.Game.Rulesets.Taiko { switch (result) { - case HitResult.SmallTickHit: - return "drum tick"; - case HitResult.SmallBonus: return "bonus"; } From 6717f0606cf904e28ebce9156937439fa7e226a5 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Fri, 5 Aug 2022 23:00:37 +0200 Subject: [PATCH 074/532] Add property to SkipOverlay --- osu.Game/Screens/Play/SkipOverlay.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 3e2cf9a756..646d2c1762 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -8,6 +8,7 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -40,6 +41,8 @@ namespace osu.Game.Screens.Play private bool isClickable; + public BindableBool IsSkippable = new()!; + [Resolved] private GameplayClock gameplayClock { get; set; } @@ -134,6 +137,7 @@ namespace osu.Game.Screens.Play remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); isClickable = progress > 0; + IsSkippable.Value = isClickable; button.Enabled.Value = isClickable; buttonContainer.State.Value = isClickable ? Visibility.Visible : Visibility.Hidden; } From 99e07aa09a0a0f5176fd9fad7c233405d02cf901 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Fri, 5 Aug 2022 23:01:52 +0200 Subject: [PATCH 075/532] Skip intro if the map gets restarted --- osu.Game/Screens/Play/Player.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9c08c77d91..91c13d95de 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -441,6 +441,12 @@ namespace osu.Game.Screens.Play }, }; + skipIntroOverlay.IsSkippable.ValueChanged += (e) => + { + if (RestartCount > 0 && e.NewValue) + skipIntroOverlay.RequestSkip.Invoke(); + }; + if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays) { skipIntroOverlay.Expire(); From d8d7423698c457258797eb63fa8c16a78b101375 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Fri, 5 Aug 2022 23:04:43 +0200 Subject: [PATCH 076/532] Reduce "wait time" in case restarting the map --- osu.Game/Screens/Play/PlayerLoader.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 674490d595..96aefd56ea 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -384,9 +384,10 @@ namespace osu.Game.Screens.Play private void contentIn() { MetadataInfo.Loading = true; + int scaleContentTime = restartCount > 0 ? 65 : 650; content.FadeInFromZero(400); - content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); + content.ScaleTo(1, scaleContentTime, Easing.OutQuint).Then().Schedule(prepareNewPlayer); lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint); highPassFilter.CutoffTo(300).Then().CutoffTo(0, 1250); // 1250 is to line up with the appearance of MetadataInfo (750 delay + 500 fade-in) @@ -468,7 +469,7 @@ namespace osu.Game.Screens.Play else this.Exit(); }); - }, 500); + }, CurrentPlayer?.RestartCount > 0 ? 50 : 500); } private void cancelLoad() From 445f9217568581fa2ed54f6f1f1727ed7f6f0a0b Mon Sep 17 00:00:00 2001 From: BlauFx Date: Fri, 5 Aug 2022 23:21:03 +0200 Subject: [PATCH 077/532] Move IsSkippable event into load method --- osu.Game/Screens/Play/Player.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 91c13d95de..e7f9e7b0d2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -365,6 +365,12 @@ namespace osu.Game.Screens.Play IsBreakTime.BindTo(breakTracker.IsBreakTime); IsBreakTime.BindValueChanged(onBreakTimeChanged, true); + + skipIntroOverlay.IsSkippable.ValueChanged += (e) => + { + if (RestartCount > 0 && e.NewValue) + skipIntroOverlay.RequestSkip.Invoke(); + }; } protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); @@ -441,12 +447,6 @@ namespace osu.Game.Screens.Play }, }; - skipIntroOverlay.IsSkippable.ValueChanged += (e) => - { - if (RestartCount > 0 && e.NewValue) - skipIntroOverlay.RequestSkip.Invoke(); - }; - if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays) { skipIntroOverlay.Expire(); From e411a2d18739f958d90dcbedfa38256489a570d7 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sat, 6 Aug 2022 15:12:36 +0200 Subject: [PATCH 078/532] Revert reduced wait time commit --- osu.Game/Screens/Play/PlayerLoader.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 96aefd56ea..674490d595 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -384,10 +384,9 @@ namespace osu.Game.Screens.Play private void contentIn() { MetadataInfo.Loading = true; - int scaleContentTime = restartCount > 0 ? 65 : 650; content.FadeInFromZero(400); - content.ScaleTo(1, scaleContentTime, Easing.OutQuint).Then().Schedule(prepareNewPlayer); + content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint); highPassFilter.CutoffTo(300).Then().CutoffTo(0, 1250); // 1250 is to line up with the appearance of MetadataInfo (750 delay + 500 fade-in) @@ -469,7 +468,7 @@ namespace osu.Game.Screens.Play else this.Exit(); }); - }, CurrentPlayer?.RestartCount > 0 ? 50 : 500); + }, 500); } private void cancelLoad() From 0d418559bc15b958dac1c75604a6596732f537dd Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sat, 6 Aug 2022 17:02:45 +0200 Subject: [PATCH 079/532] Skip song intro only in case of a quick restart --- osu.Game/Screens/Play/Player.cs | 7 ++++++- osu.Game/Screens/Play/PlayerLoader.cs | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e7f9e7b0d2..a0e513648a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -81,6 +81,10 @@ namespace osu.Game.Screens.Play private bool isRestarting; + public bool RestartedViaHotkey; + + public Bindable IsRestartingFromHotkey = new Bindable(); + private Bindable mouseWheelDisabled; private readonly Bindable storyboardReplacesBackground = new Bindable(); @@ -287,6 +291,7 @@ namespace osu.Game.Screens.Play { if (!this.IsCurrentScreen()) return; + IsRestartingFromHotkey.Value = true; fadeOut(true); Restart(); }, @@ -368,7 +373,7 @@ namespace osu.Game.Screens.Play skipIntroOverlay.IsSkippable.ValueChanged += (e) => { - if (RestartCount > 0 && e.NewValue) + if (RestartedViaHotkey && e.NewValue && RestartCount > 0) skipIntroOverlay.RequestSkip.Invoke(); }; } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 674490d595..388d7d6ec5 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -123,6 +123,8 @@ namespace osu.Game.Screens.Play private EpilepsyWarning? epilepsyWarning; + private bool isHotKeyRestart; + [Resolved(CanBeNull = true)] private INotificationOverlay? notificationOverlay { get; set; } @@ -363,9 +365,20 @@ namespace osu.Game.Screens.Play CurrentPlayer = createPlayer(); CurrentPlayer.RestartCount = restartCount++; CurrentPlayer.RestartRequested = restartRequested; + CurrentPlayer.IsRestartingFromHotkey.ValueChanged += e => + { + if (e.NewValue) + isHotKeyRestart = true; + }; LoadTask = LoadComponentAsync(CurrentPlayer, _ => { + if (isHotKeyRestart) + { + CurrentPlayer.RestartedViaHotkey = true; + isHotKeyRestart = false; + } + MetadataInfo.Loading = false; OnPlayerLoaded(); }); From fa6d55b5b5e69d4c61c965b02d42e84eec86c911 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sat, 6 Aug 2022 18:47:11 +0200 Subject: [PATCH 080/532] Remove redundant lambda signature parentheses --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a0e513648a..3087b56833 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -371,7 +371,7 @@ namespace osu.Game.Screens.Play IsBreakTime.BindTo(breakTracker.IsBreakTime); IsBreakTime.BindValueChanged(onBreakTimeChanged, true); - skipIntroOverlay.IsSkippable.ValueChanged += (e) => + skipIntroOverlay.IsSkippable.ValueChanged += e => { if (RestartedViaHotkey && e.NewValue && RestartCount > 0) skipIntroOverlay.RequestSkip.Invoke(); From bd43a9e96e377f4c57ef639cb180f1a62576880e Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sat, 6 Aug 2022 18:49:07 +0200 Subject: [PATCH 081/532] Add missing type specification --- osu.Game/Screens/Play/SkipOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 646d2c1762..a5f6bf3fee 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play private bool isClickable; - public BindableBool IsSkippable = new()!; + public BindableBool IsSkippable = new BindableBool()!; [Resolved] private GameplayClock gameplayClock { get; set; } From 09230304a4f49df8622391b49bab292fd54123f5 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sun, 7 Aug 2022 13:20:29 +0200 Subject: [PATCH 082/532] Improve implementation --- osu.Game/Screens/Play/Player.cs | 11 +++++------ osu.Game/Screens/Play/PlayerConfiguration.cs | 2 ++ osu.Game/Screens/Play/PlayerLoader.cs | 12 ++++++------ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3087b56833..63e7bcb115 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -81,10 +81,6 @@ namespace osu.Game.Screens.Play private bool isRestarting; - public bool RestartedViaHotkey; - - public Bindable IsRestartingFromHotkey = new Bindable(); - private Bindable mouseWheelDisabled; private readonly Bindable storyboardReplacesBackground = new Bindable(); @@ -291,7 +287,7 @@ namespace osu.Game.Screens.Play { if (!this.IsCurrentScreen()) return; - IsRestartingFromHotkey.Value = true; + Configuration.AutomaticallySkipIntro = true; fadeOut(true); Restart(); }, @@ -373,8 +369,11 @@ namespace osu.Game.Screens.Play skipIntroOverlay.IsSkippable.ValueChanged += e => { - if (RestartedViaHotkey && e.NewValue && RestartCount > 0) + if (Configuration.AutomaticallySkipIntro && e.NewValue && RestartCount > 0) + { + Configuration.AutomaticallySkipIntro = false; skipIntroOverlay.RequestSkip.Invoke(); + } }; } diff --git a/osu.Game/Screens/Play/PlayerConfiguration.cs b/osu.Game/Screens/Play/PlayerConfiguration.cs index d11825baee..0624508584 100644 --- a/osu.Game/Screens/Play/PlayerConfiguration.cs +++ b/osu.Game/Screens/Play/PlayerConfiguration.cs @@ -31,5 +31,7 @@ namespace osu.Game.Screens.Play /// Whether the player should be allowed to skip intros/outros, advancing to the start of gameplay or the end of a storyboard. /// public bool AllowSkipping { get; set; } = true; + + public bool AutomaticallySkipIntro { get; set; } } } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 388d7d6ec5..477811a979 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -365,17 +365,12 @@ namespace osu.Game.Screens.Play CurrentPlayer = createPlayer(); CurrentPlayer.RestartCount = restartCount++; CurrentPlayer.RestartRequested = restartRequested; - CurrentPlayer.IsRestartingFromHotkey.ValueChanged += e => - { - if (e.NewValue) - isHotKeyRestart = true; - }; LoadTask = LoadComponentAsync(CurrentPlayer, _ => { if (isHotKeyRestart) { - CurrentPlayer.RestartedViaHotkey = true; + CurrentPlayer.Configuration.AutomaticallySkipIntro = true; isHotKeyRestart = false; } @@ -390,6 +385,11 @@ namespace osu.Game.Screens.Play private void restartRequested() { + if (CurrentPlayer != null) + { + isHotKeyRestart = CurrentPlayer.Configuration.AutomaticallySkipIntro; + } + hideOverlays = true; ValidForResume = true; } From bb344e064fafb25878e006e30c5e77afd9df432b Mon Sep 17 00:00:00 2001 From: BlauFx Date: Sun, 7 Aug 2022 13:31:26 +0200 Subject: [PATCH 083/532] Add xml docs --- osu.Game/Screens/Play/PlayerConfiguration.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerConfiguration.cs b/osu.Game/Screens/Play/PlayerConfiguration.cs index 0624508584..b1b0e01d80 100644 --- a/osu.Game/Screens/Play/PlayerConfiguration.cs +++ b/osu.Game/Screens/Play/PlayerConfiguration.cs @@ -32,6 +32,9 @@ namespace osu.Game.Screens.Play /// public bool AllowSkipping { get; set; } = true; + /// + /// Whether the intro should be skipped by default. + /// public bool AutomaticallySkipIntro { get; set; } } } From cd68134565d6a02d014b8d80ae37738cf737741a Mon Sep 17 00:00:00 2001 From: BlauFx Date: Mon, 8 Aug 2022 13:10:28 +0200 Subject: [PATCH 084/532] Call skip method directly --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 63e7bcb115..b648c918e1 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -372,7 +372,7 @@ namespace osu.Game.Screens.Play if (Configuration.AutomaticallySkipIntro && e.NewValue && RestartCount > 0) { Configuration.AutomaticallySkipIntro = false; - skipIntroOverlay.RequestSkip.Invoke(); + performUserRequestedSkip(); } }; } From fac2596eee86b21e49fc9e065258208009aa16d2 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Mon, 8 Aug 2022 13:38:52 +0200 Subject: [PATCH 085/532] Change type from BindableBool to IBindable --- osu.Game/Screens/Play/SkipOverlay.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index a5f6bf3fee..227038253d 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play private bool isClickable; - public BindableBool IsSkippable = new BindableBool()!; + public IBindable IsSkippable = new Bindable(); [Resolved] private GameplayClock gameplayClock { get; set; } @@ -92,6 +92,8 @@ namespace osu.Game.Screens.Play } } }; + + IsSkippable.BindTo(button.Enabled); } private const double fade_time = 300; @@ -137,7 +139,6 @@ namespace osu.Game.Screens.Play remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); isClickable = progress > 0; - IsSkippable.Value = isClickable; button.Enabled.Value = isClickable; buttonContainer.State.Value = isClickable ? Visibility.Visible : Visibility.Hidden; } From f6e65cf1af289afdb6a3706321c16435057bcdda Mon Sep 17 00:00:00 2001 From: BlauFx Date: Mon, 8 Aug 2022 20:53:05 +0200 Subject: [PATCH 086/532] Improve implementation --- osu.Game/Screens/Play/Player.cs | 10 ++++++---- osu.Game/Screens/Play/PlayerLoader.cs | 4 +--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b648c918e1..f2b3bfc090 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -81,6 +81,11 @@ namespace osu.Game.Screens.Play private bool isRestarting; + /// + /// Is set to true when pressed the via the quick retry hotkey. + /// + public Bindable IsQuickRestart = new Bindable(); + private Bindable mouseWheelDisabled; private readonly Bindable storyboardReplacesBackground = new Bindable(); @@ -287,7 +292,7 @@ namespace osu.Game.Screens.Play { if (!this.IsCurrentScreen()) return; - Configuration.AutomaticallySkipIntro = true; + IsQuickRestart.Value = true; fadeOut(true); Restart(); }, @@ -370,10 +375,7 @@ namespace osu.Game.Screens.Play skipIntroOverlay.IsSkippable.ValueChanged += e => { if (Configuration.AutomaticallySkipIntro && e.NewValue && RestartCount > 0) - { - Configuration.AutomaticallySkipIntro = false; performUserRequestedSkip(); - } }; } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 477811a979..76092fbaa3 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -386,9 +386,7 @@ namespace osu.Game.Screens.Play private void restartRequested() { if (CurrentPlayer != null) - { - isHotKeyRestart = CurrentPlayer.Configuration.AutomaticallySkipIntro; - } + isHotKeyRestart = CurrentPlayer.IsQuickRestart.Value; hideOverlays = true; ValidForResume = true; From e4879aa4509dfae0942989d2ca67cddfb7338743 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Mon, 8 Aug 2022 21:12:38 +0200 Subject: [PATCH 087/532] Add test --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 05474e3d39..b0308dcd22 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -353,6 +353,22 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); } + [Test] + public void TestQuickRetry() + { + AddStep("load dummy beatmap", () => resetPlayer(false)); + AddUntilStep("wait for current", () => player.IsCurrentScreen()); + + AddStep("Restart map normally", () => player.Restart()); + AddUntilStep("wait for current", () => player.IsCurrentScreen()); + + AddStep("Restart map with quick retry hotkey", () => + { + InputManager.UseParentInput = true; + InputManager.PressKey(Key.Tilde); + }); + } + private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(); private class TestPlayerLoader : PlayerLoader From 0afa3a5ec8eb62e4bda6c8476fa03808af736259 Mon Sep 17 00:00:00 2001 From: BlauFx Date: Mon, 8 Aug 2022 21:20:09 +0200 Subject: [PATCH 088/532] Fix xml doc --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f2b3bfc090..1dd4006450 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -82,7 +82,7 @@ namespace osu.Game.Screens.Play private bool isRestarting; /// - /// Is set to true when pressed the via the quick retry hotkey. + /// Is set to true when the quick retry hotkey has been pressed. /// public Bindable IsQuickRestart = new Bindable(); From f65b7ef058b4ad3a20f9df593fcd4fb428ac4660 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Tue, 9 Aug 2022 02:49:53 -0400 Subject: [PATCH 089/532] Add keybind for showing profile --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++++ osu.Game/Localisation/GlobalActionKeyBindingStrings.cs | 5 +++++ osu.Game/OsuGame.cs | 4 ++++ osu.Game/Overlays/Toolbar/ToolbarUserButton.cs | 1 + 4 files changed, 14 insertions(+) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 1ee03a6964..d7a3bbba55 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -52,6 +52,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing), new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), + new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ShowProfile), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor), new KeyBinding(InputKey.Escape, GlobalAction.Back), @@ -232,6 +233,9 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleNotifications))] ToggleNotifications, + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ShowProfile))] + ShowProfile, + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.PauseGameplay))] PauseGameplay, diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index de1a5b189c..9089834c30 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -149,6 +149,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ToggleNotifications => new TranslatableString(getKey(@"toggle_notifications"), @"Toggle notifications"); + /// + /// "Show Profile" + /// + public static LocalisableString ShowProfile => new TranslatableString(getKey(@"toggle_notifications"), @"Show Profile"); + /// /// "Pause gameplay" /// diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 78cc4d7f70..9c160d39fa 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1138,6 +1138,10 @@ namespace osu.Game mouseDisableButtons.Value = !mouseDisableButtons.Value; return true; + case GlobalAction.ShowProfile: + ShowUser(new APIUser { Id = API.LocalUser.Value.Id }); + 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. diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index a93ba17c5b..ef8157394b 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -15,6 +15,7 @@ using osu.Game.Resources.Localisation.Web; using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; +using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar { From 3473347f35335877eb8c7ecbb4c134505d885302 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Tue, 9 Aug 2022 02:56:12 -0400 Subject: [PATCH 090/532] Lowercase "p" --- osu.Game/Localisation/GlobalActionKeyBindingStrings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 9089834c30..e28ec4eb09 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -150,9 +150,9 @@ namespace osu.Game.Localisation public static LocalisableString ToggleNotifications => new TranslatableString(getKey(@"toggle_notifications"), @"Toggle notifications"); /// - /// "Show Profile" + /// "Show profile" /// - public static LocalisableString ShowProfile => new TranslatableString(getKey(@"toggle_notifications"), @"Show Profile"); + public static LocalisableString ShowProfile => new TranslatableString(getKey(@"toggle_notifications"), @"Show profile"); /// /// "Pause gameplay" From ededaed5ef3dad1798063ed698efda25cf4b35b1 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Tue, 9 Aug 2022 02:58:28 -0400 Subject: [PATCH 091/532] Remove unused import --- osu.Game/Overlays/Toolbar/ToolbarUserButton.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index ef8157394b..a93ba17c5b 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -15,7 +15,6 @@ using osu.Game.Resources.Localisation.Web; using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; -using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar { From 04108a749e9703be3349fb3c278513854be390f2 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Tue, 9 Aug 2022 03:03:14 -0400 Subject: [PATCH 092/532] Rename translation key --- osu.Game/Localisation/GlobalActionKeyBindingStrings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index e28ec4eb09..0624a5b34b 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -152,7 +152,7 @@ namespace osu.Game.Localisation /// /// "Show profile" /// - public static LocalisableString ShowProfile => new TranslatableString(getKey(@"toggle_notifications"), @"Show profile"); + public static LocalisableString ShowProfile => new TranslatableString(getKey(@"show_profile"), @"Show profile"); /// /// "Pause gameplay" From a705c4f5d2ebfdf4d2f79758500b1ddc13e40d7e Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Tue, 9 Aug 2022 03:17:55 -0400 Subject: [PATCH 093/532] Moved ShowProfile to the bottom of the enum --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index d7a3bbba55..4fc85b135f 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -233,9 +233,6 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleNotifications))] ToggleNotifications, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ShowProfile))] - ShowProfile, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.PauseGameplay))] PauseGameplay, @@ -336,5 +333,8 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleFPSCounter))] ToggleFPSDisplay, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ShowProfile))] + ShowProfile, } } From aee18135a98be2610af30321d238328d04c1b59f Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Tue, 9 Aug 2022 04:09:22 -0400 Subject: [PATCH 094/532] Switch to toggle --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 6 +++--- osu.Game/Localisation/GlobalActionKeyBindingStrings.cs | 4 ++-- osu.Game/OsuGame.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 4fc85b135f..8b69e22324 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -52,7 +52,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing), new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), - new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ShowProfile), + new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor), new KeyBinding(InputKey.Escape, GlobalAction.Back), @@ -334,7 +334,7 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleFPSCounter))] ToggleFPSDisplay, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ShowProfile))] - ShowProfile, + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleProfile))] + ToggleProfile, } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 0624a5b34b..172818c1c0 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -150,9 +150,9 @@ namespace osu.Game.Localisation public static LocalisableString ToggleNotifications => new TranslatableString(getKey(@"toggle_notifications"), @"Toggle notifications"); /// - /// "Show profile" + /// "Toggle profile" /// - public static LocalisableString ShowProfile => new TranslatableString(getKey(@"show_profile"), @"Show profile"); + public static LocalisableString ToggleProfile => new TranslatableString(getKey(@"toggle_profile"), @"Toggle profile"); /// /// "Pause gameplay" diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 9c160d39fa..f0d61ed07a 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1138,7 +1138,7 @@ namespace osu.Game mouseDisableButtons.Value = !mouseDisableButtons.Value; return true; - case GlobalAction.ShowProfile: + case GlobalAction.ToggleProfile: ShowUser(new APIUser { Id = API.LocalUser.Value.Id }); return true; From 7ed489b56d47c7e96758f9a17fdb02e300443724 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Tue, 9 Aug 2022 14:10:38 -0400 Subject: [PATCH 095/532] Add hotkey to Toolbar --- osu.Game/OsuGame.cs | 4 ---- osu.Game/Overlays/Toolbar/ToolbarUserButton.cs | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index f0d61ed07a..78cc4d7f70 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1138,10 +1138,6 @@ namespace osu.Game mouseDisableButtons.Value = !mouseDisableButtons.Value; return true; - case GlobalAction.ToggleProfile: - ShowUser(new APIUser { Id = API.LocalUser.Value.Id }); - 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. diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index 4ebd19a1f7..35f72f0ff8 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -15,6 +15,7 @@ using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Users.Drawables; +using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; @@ -34,6 +35,8 @@ namespace osu.Game.Overlays.Toolbar public ToolbarUserButton() { + Hotkey = GlobalAction.ToggleProfile; + AutoSizeAxes = Axes.X; } From 8c7ede611162bfb64faf86927e7390812bfacf8c Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Tue, 9 Aug 2022 14:43:37 -0400 Subject: [PATCH 096/532] Add proper toggling --- osu.Game/OsuGame.cs | 7 +++++++ osu.Game/Overlays/Toolbar/ToolbarUserButton.cs | 3 --- osu.Game/Overlays/UserProfileOverlay.cs | 13 +++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 78cc4d7f70..b89a0dc843 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1138,6 +1138,13 @@ namespace osu.Game mouseDisableButtons.Value = !mouseDisableButtons.Value; return true; + case GlobalAction.ToggleProfile: + if (userProfile.State.Value == Visibility.Visible) + return false; + + ShowUser(new APIUser { Id = API.LocalUser.Value.Id }); + 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. diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index 35f72f0ff8..4ebd19a1f7 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -15,7 +15,6 @@ using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Users.Drawables; -using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; @@ -35,8 +34,6 @@ namespace osu.Game.Overlays.Toolbar public ToolbarUserButton() { - Hotkey = GlobalAction.ToggleProfile; - AutoSizeAxes = Axes.X; } diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index c0ca63bbd9..fb119b92f2 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -19,6 +19,7 @@ using osu.Game.Overlays.Profile.Sections; using osu.Game.Users; using osuTK; using osuTK.Graphics; +using osu.Game.Input.Bindings; namespace osu.Game.Overlays { @@ -41,6 +42,18 @@ namespace osu.Game.Overlays protected override Color4 BackgroundColour => ColourProvider.Background6; + public override bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.ToggleProfile: + Hide(); + return true; + } + + return base.OnPressed(e); + } + public void ShowUser(IUser user) { if (user.OnlineID == APIUser.SYSTEM_USER_ID) From 396860d9e8f4a804fd7a72304f88dbf5de153e8d Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 10 Aug 2022 13:31:58 -0400 Subject: [PATCH 097/532] Move Hide() to OsuGame --- osu.Game/OsuGame.cs | 12 +++++++++--- osu.Game/Overlays/UserProfileOverlay.cs | 13 ------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index b89a0dc843..ca0ce916a2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1140,10 +1140,16 @@ namespace osu.Game case GlobalAction.ToggleProfile: if (userProfile.State.Value == Visibility.Visible) - return false; + { + userProfile.Hide(); + return true; + } - ShowUser(new APIUser { Id = API.LocalUser.Value.Id }); - return true; + else + { + ShowUser(new APIUser { Id = API.LocalUser.Value.Id }); + return true; + } case GlobalAction.RandomSkin: // Don't allow random skin selection while in the skin editor. diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index fb119b92f2..c0ca63bbd9 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -19,7 +19,6 @@ using osu.Game.Overlays.Profile.Sections; using osu.Game.Users; using osuTK; using osuTK.Graphics; -using osu.Game.Input.Bindings; namespace osu.Game.Overlays { @@ -42,18 +41,6 @@ namespace osu.Game.Overlays protected override Color4 BackgroundColour => ColourProvider.Background6; - public override bool OnPressed(KeyBindingPressEvent e) - { - switch (e.Action) - { - case GlobalAction.ToggleProfile: - Hide(); - return true; - } - - return base.OnPressed(e); - } - public void ShowUser(IUser user) { if (user.OnlineID == APIUser.SYSTEM_USER_ID) From 2499b7f0cd70cb23a2b298fb4e885a05924ef283 Mon Sep 17 00:00:00 2001 From: its5Q Date: Thu, 11 Aug 2022 03:53:20 +1000 Subject: [PATCH 098/532] Add localisation support for beatmap editor setup --- .../Graphics/UserInterfaceV2/ColourPalette.cs | 5 +- .../UserInterfaceV2/LabelledColourPalette.cs | 3 +- .../Localisation/EditorSetupColoursStrings.cs | 24 ++++++ .../Localisation/EditorSetupDesignStrings.cs | 84 +++++++++++++++++++ .../EditorSetupDifficultyStrings.cs | 34 ++++++++ .../EditorSetupMetadataStrings.cs | 39 +++++++++ .../EditorSetupResourcesStrings.cs | 44 ++++++++++ .../Localisation/EditorSetupRulesetStrings.cs | 19 +++++ osu.Game/Localisation/EditorSetupStrings.cs | 24 ++++++ osu.Game/Screens/Edit/Setup/ColoursSection.cs | 8 +- osu.Game/Screens/Edit/Setup/DesignSection.cs | 29 +++---- .../Screens/Edit/Setup/DifficultySection.cs | 11 +-- .../Screens/Edit/Setup/MetadataSection.cs | 15 ++-- .../Screens/Edit/Setup/ResourcesSection.cs | 15 ++-- .../Screens/Edit/Setup/RulesetSetupSection.cs | 3 +- .../Screens/Edit/Setup/SetupScreenHeader.cs | 5 +- 16 files changed, 320 insertions(+), 42 deletions(-) create mode 100644 osu.Game/Localisation/EditorSetupColoursStrings.cs create mode 100644 osu.Game/Localisation/EditorSetupDesignStrings.cs create mode 100644 osu.Game/Localisation/EditorSetupDifficultyStrings.cs create mode 100644 osu.Game/Localisation/EditorSetupMetadataStrings.cs create mode 100644 osu.Game/Localisation/EditorSetupResourcesStrings.cs create mode 100644 osu.Game/Localisation/EditorSetupRulesetStrings.cs create mode 100644 osu.Game/Localisation/EditorSetupStrings.cs diff --git a/osu.Game/Graphics/UserInterfaceV2/ColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/ColourPalette.cs index 001ccc7f87..f61d6db8b1 100644 --- a/osu.Game/Graphics/UserInterfaceV2/ColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/ColourPalette.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osuTK; @@ -26,9 +27,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 { public BindableList Colours { get; } = new BindableList(); - private string colourNamePrefix = "Colour"; + private LocalisableString colourNamePrefix = "Colour"; - public string ColourNamePrefix + public LocalisableString ColourNamePrefix { get => colourNamePrefix; set diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs index 7b9684e3ef..b144f8f696 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Localisation; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -17,7 +18,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 public BindableList Colours => Component.Colours; - public string ColourNamePrefix + public LocalisableString ColourNamePrefix { get => Component.ColourNamePrefix; set => Component.ColourNamePrefix = value; diff --git a/osu.Game/Localisation/EditorSetupColoursStrings.cs b/osu.Game/Localisation/EditorSetupColoursStrings.cs new file mode 100644 index 0000000000..e08240a7d2 --- /dev/null +++ b/osu.Game/Localisation/EditorSetupColoursStrings.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class EditorSetupColoursStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.EditorSetupColours"; + + /// + /// "Colours" + /// + public static LocalisableString Colours => new TranslatableString(getKey(@"colours"), @"Colours"); + + /// + /// "Hitcircle / Slider Combos" + /// + public static LocalisableString HitcircleSliderCombos => new TranslatableString(getKey(@"hitcircle_slider_combos"), @"Hitcircle / Slider Combos"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/EditorSetupDesignStrings.cs b/osu.Game/Localisation/EditorSetupDesignStrings.cs new file mode 100644 index 0000000000..0a5e383b76 --- /dev/null +++ b/osu.Game/Localisation/EditorSetupDesignStrings.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class EditorSetupDesignStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.EditorSetupDesign"; + + /// + /// "Design" + /// + public static LocalisableString Design => new TranslatableString(getKey(@"design"), @"Design"); + + /// + /// "Enable countdown" + /// + public static LocalisableString EnableCountdown => new TranslatableString(getKey(@"enable_countdown"), @"Enable countdown"); + + /// + /// "If enabled, an "Are you ready? 3, 2, 1, GO!" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so." + /// + public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"), @"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."); + + /// + /// "Countdown speed" + /// + public static LocalisableString CountdownSpeed => new TranslatableString(getKey(@"countdown_speed"), @"Countdown speed"); + + /// + /// "If the countdown sounds off-time, use this to make it appear one or more beats early." + /// + public static LocalisableString CountdownOffsetDescription => new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early."); + + /// + /// "Countdown offset" + /// + public static LocalisableString CountdownOffset => new TranslatableString(getKey(@"countdown_offset"), @"Countdown offset"); + + /// + /// "Widescreen support" + /// + public static LocalisableString WidescreenSupport => new TranslatableString(getKey(@"widescreen_support"), @"Widescreen support"); + + /// + /// "Allows storyboards to use the full screen space, rather than be confined to a 4:3 area." + /// + public static LocalisableString WidescreenSupportDescription => new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."); + + /// + /// "Epilepsy warning" + /// + public static LocalisableString EpilepsyWarning => new TranslatableString(getKey(@"epilepsy_warning"), @"Epilepsy warning"); + + /// + /// "Recommended if the storyboard or video contain scenes with rapidly flashing colours." + /// + public static LocalisableString EpilepsyWarningDescription => new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours."); + + /// + /// "Letterbox during breaks" + /// + public static LocalisableString LetterboxDuringBreaks => new TranslatableString(getKey(@"letterbox_during_breaks"), @"Letterbox during breaks"); + + /// + /// "Adds horizontal letterboxing to give a cinematic look during breaks." + /// + public static LocalisableString LetterboxDuringBreaksDescription => new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks."); + + /// + /// "Samples match playback rate" + /// + public static LocalisableString SamplesMatchPlaybackRate => new TranslatableString(getKey(@"samples_match_playback_rate"), @"Samples match playback rate"); + + /// + /// "When enabled, all samples will speed up or slow down when rate-changing mods are enabled." + /// + public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"), @"When enabled, all samples will speed up or slow down when rate-changing mods are enabled."); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/EditorSetupDifficultyStrings.cs b/osu.Game/Localisation/EditorSetupDifficultyStrings.cs new file mode 100644 index 0000000000..cdb7837a64 --- /dev/null +++ b/osu.Game/Localisation/EditorSetupDifficultyStrings.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class EditorSetupDifficultyStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.EditorSetupDifficulty"; + + /// + /// "The size of all hit objects" + /// + public static LocalisableString CircleSizeDescription => new TranslatableString(getKey(@"circle_size_description"), @"The size of all hit objects"); + + /// + /// "The rate of passive health drain throughout playable time" + /// + public static LocalisableString DrainRateDescription => new TranslatableString(getKey(@"drain_rate_description"), @"The rate of passive health drain throughout playable time"); + + /// + /// "The speed at which objects are presented to the player" + /// + public static LocalisableString ApproachRateDescription => new TranslatableString(getKey(@"approach_rate_description"), @"The speed at which objects are presented to the player"); + + /// + /// "The harshness of hit windows and difficulty of special objects (ie. spinners)" + /// + public static LocalisableString OverallDifficultyDescription => new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/EditorSetupMetadataStrings.cs b/osu.Game/Localisation/EditorSetupMetadataStrings.cs new file mode 100644 index 0000000000..576fa68643 --- /dev/null +++ b/osu.Game/Localisation/EditorSetupMetadataStrings.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class EditorSetupMetadataStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.EditorSetupMetadata"; + + /// + /// "Metadata" + /// + public static LocalisableString Metadata => new TranslatableString(getKey(@"metadata"), @"Metadata"); + + /// + /// "Romanised Artist" + /// + public static LocalisableString RomanisedArtist => new TranslatableString(getKey(@"romanised_artist"), @"Romanised Artist"); + + /// + /// "Romanised Title" + /// + public static LocalisableString RomanisedTitle => new TranslatableString(getKey(@"romanised_title"), @"Romanised Title"); + + /// + /// "Creator" + /// + public static LocalisableString Creator => new TranslatableString(getKey(@"creator"), @"Creator"); + + /// + /// "Difficulty Name" + /// + public static LocalisableString DifficultyName => new TranslatableString(getKey(@"difficulty_name"), @"Difficulty Name"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/EditorSetupResourcesStrings.cs b/osu.Game/Localisation/EditorSetupResourcesStrings.cs new file mode 100644 index 0000000000..493beae7fe --- /dev/null +++ b/osu.Game/Localisation/EditorSetupResourcesStrings.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class EditorSetupResourcesStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.EditorSetupResources"; + + /// + /// "Resources" + /// + public static LocalisableString Resources => new TranslatableString(getKey(@"resources"), @"Resources"); + + /// + /// "Audio Track" + /// + public static LocalisableString AudioTrack => new TranslatableString(getKey(@"audio_track"), @"Audio Track"); + + /// + /// "Click to select a track" + /// + public static LocalisableString ClickToSelectTrack => new TranslatableString(getKey(@"click_to_select_track"), @"Click to select a track"); + + /// + /// "Click to replace the track" + /// + public static LocalisableString ClickToReplaceTrack => new TranslatableString(getKey(@"click_to_replace_track"), @"Click to replace the track"); + + /// + /// "Click to select a background image" + /// + public static LocalisableString ClickToSelectBackground => new TranslatableString(getKey(@"click_to_select_background"), @"Click to select a background image"); + + /// + /// "Click to replace the background image" + /// + public static LocalisableString ClickToReplaceBackground => new TranslatableString(getKey(@"click_to_replace_background"), @"Click to replace the background image"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/EditorSetupRulesetStrings.cs b/osu.Game/Localisation/EditorSetupRulesetStrings.cs new file mode 100644 index 0000000000..a786b679a3 --- /dev/null +++ b/osu.Game/Localisation/EditorSetupRulesetStrings.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class EditorSetupRulesetStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.EditorSetupRuleset"; + + /// + /// "Ruleset ({0})" + /// + public static LocalisableString Ruleset(string arg0) => new TranslatableString(getKey(@"ruleset"), @"Ruleset ({0})", arg0); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/EditorSetupStrings.cs b/osu.Game/Localisation/EditorSetupStrings.cs new file mode 100644 index 0000000000..97ebd40b6f --- /dev/null +++ b/osu.Game/Localisation/EditorSetupStrings.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class EditorSetupStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.EditorSetup"; + + /// + /// "beatmap setup" + /// + public static LocalisableString BeatmapSetup => new TranslatableString(getKey(@"beatmap_setup"), @"beatmap setup"); + + /// + /// "change general settings of your beatmap" + /// + public static LocalisableString BeatmapSetupDescription => new TranslatableString(getKey(@"beatmap_setup_description"), @"change general settings of your beatmap"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Screens/Edit/Setup/ColoursSection.cs b/osu.Game/Screens/Edit/Setup/ColoursSection.cs index 621abf5580..5792613aa0 100644 --- a/osu.Game/Screens/Edit/Setup/ColoursSection.cs +++ b/osu.Game/Screens/Edit/Setup/ColoursSection.cs @@ -7,12 +7,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Screens.Edit.Setup { internal class ColoursSection : SetupSection { - public override LocalisableString Title => "Colours"; + public override LocalisableString Title => EditorSetupColoursStrings.Colours; private LabelledColourPalette comboColours; @@ -23,9 +25,9 @@ namespace osu.Game.Screens.Edit.Setup { comboColours = new LabelledColourPalette { - Label = "Hitcircle / Slider Combos", + Label = EditorSetupColoursStrings.HitcircleSliderCombos, FixedLabelWidth = LABEL_WIDTH, - ColourNamePrefix = "Combo" + ColourNamePrefix = MatchesStrings.MatchScoreStatsCombo } }; diff --git a/osu.Game/Screens/Edit/Setup/DesignSection.cs b/osu.Game/Screens/Edit/Setup/DesignSection.cs index 40bbfeaf7d..6214a17529 100644 --- a/osu.Game/Screens/Edit/Setup/DesignSection.cs +++ b/osu.Game/Screens/Edit/Setup/DesignSection.cs @@ -13,6 +13,7 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Screens.Edit.Setup { @@ -29,7 +30,7 @@ namespace osu.Game.Screens.Edit.Setup private LabelledSwitchButton letterboxDuringBreaks; private LabelledSwitchButton samplesMatchPlaybackRate; - public override LocalisableString Title => "Design"; + public override LocalisableString Title => EditorSetupDesignStrings.Design; [BackgroundDependencyLoader] private void load() @@ -38,9 +39,9 @@ namespace osu.Game.Screens.Edit.Setup { EnableCountdown = new LabelledSwitchButton { - Label = "Enable countdown", + Label = EditorSetupDesignStrings.EnableCountdown, Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None }, - Description = "If enabled, an \"Are you ready? 3, 2, 1, GO!\" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so." + Description = EditorSetupDesignStrings.CountdownDescription }, CountdownSettings = new FillFlowContainer { @@ -52,41 +53,41 @@ namespace osu.Game.Screens.Edit.Setup { CountdownSpeed = new LabelledEnumDropdown { - Label = "Countdown speed", + Label = EditorSetupDesignStrings.CountdownSpeed, Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None ? Beatmap.BeatmapInfo.Countdown : CountdownType.Normal }, Items = Enum.GetValues(typeof(CountdownType)).Cast().Where(type => type != CountdownType.None) }, CountdownOffset = new LabelledNumberBox { - Label = "Countdown offset", + Label = EditorSetupDesignStrings.CountdownOffset, Current = { Value = Beatmap.BeatmapInfo.CountdownOffset.ToString() }, - Description = "If the countdown sounds off-time, use this to make it appear one or more beats early.", + Description = EditorSetupDesignStrings.CountdownOffsetDescription, } } }, Empty(), widescreenSupport = new LabelledSwitchButton { - Label = "Widescreen support", - Description = "Allows storyboards to use the full screen space, rather than be confined to a 4:3 area.", + Label = EditorSetupDesignStrings.WidescreenSupport, + Description = EditorSetupDesignStrings.WidescreenSupportDescription, Current = { Value = Beatmap.BeatmapInfo.WidescreenStoryboard } }, epilepsyWarning = new LabelledSwitchButton { - Label = "Epilepsy warning", - Description = "Recommended if the storyboard or video contain scenes with rapidly flashing colours.", + Label = EditorSetupDesignStrings.EpilepsyWarning, + Description = EditorSetupDesignStrings.EpilepsyWarningDescription, Current = { Value = Beatmap.BeatmapInfo.EpilepsyWarning } }, letterboxDuringBreaks = new LabelledSwitchButton { - Label = "Letterbox during breaks", - Description = "Adds horizontal letterboxing to give a cinematic look during breaks.", + Label = EditorSetupDesignStrings.LetterboxDuringBreaks, + Description = EditorSetupDesignStrings.LetterboxDuringBreaksDescription, Current = { Value = Beatmap.BeatmapInfo.LetterboxInBreaks } }, samplesMatchPlaybackRate = new LabelledSwitchButton { - Label = "Samples match playback rate", - Description = "When enabled, all samples will speed up or slow down when rate-changing mods are enabled.", + Label = EditorSetupDesignStrings.SamplesMatchPlaybackRate, + Description = EditorSetupDesignStrings.SamplesMatchPlaybackRateDescription, Current = { Value = Beatmap.BeatmapInfo.SamplesMatchPlaybackRate } } }; diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index 5ce5d05d64..d90997653c 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -11,6 +11,7 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Resources.Localisation.Web; +using osu.Game.Localisation; namespace osu.Game.Screens.Edit.Setup { @@ -21,7 +22,7 @@ namespace osu.Game.Screens.Edit.Setup private LabelledSliderBar approachRateSlider; private LabelledSliderBar overallDifficultySlider; - public override LocalisableString Title => "Difficulty"; + public override LocalisableString Title => BeatmapDiscussionsStrings.OwnerEditorVersion; [BackgroundDependencyLoader] private void load() @@ -32,7 +33,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = BeatmapsetsStrings.ShowStatsCs, FixedLabelWidth = LABEL_WIDTH, - Description = "The size of all hit objects", + Description = EditorSetupDifficultyStrings.CircleSizeDescription, Current = new BindableFloat(Beatmap.Difficulty.CircleSize) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, @@ -45,7 +46,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = BeatmapsetsStrings.ShowStatsDrain, FixedLabelWidth = LABEL_WIDTH, - Description = "The rate of passive health drain throughout playable time", + Description = EditorSetupDifficultyStrings.DrainRateDescription, Current = new BindableFloat(Beatmap.Difficulty.DrainRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, @@ -58,7 +59,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = BeatmapsetsStrings.ShowStatsAr, FixedLabelWidth = LABEL_WIDTH, - Description = "The speed at which objects are presented to the player", + Description = EditorSetupDifficultyStrings.ApproachRateDescription, Current = new BindableFloat(Beatmap.Difficulty.ApproachRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, @@ -71,7 +72,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = BeatmapsetsStrings.ShowStatsAccuracy, FixedLabelWidth = LABEL_WIDTH, - Description = "The harshness of hit windows and difficulty of special objects (ie. spinners)", + Description = EditorSetupDifficultyStrings.OverallDifficultyDescription, Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 854dec2001..1af749160b 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -10,6 +10,7 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Resources.Localisation.Web; +using osu.Game.Localisation; namespace osu.Game.Screens.Edit.Setup { @@ -26,7 +27,7 @@ namespace osu.Game.Screens.Edit.Setup private LabelledTextBox sourceTextBox; private LabelledTextBox tagsTextBox; - public override LocalisableString Title => "Metadata"; + public override LocalisableString Title => EditorSetupMetadataStrings.Metadata; [BackgroundDependencyLoader] private void load() @@ -35,22 +36,22 @@ namespace osu.Game.Screens.Edit.Setup Children = new[] { - ArtistTextBox = createTextBox("Artist", + ArtistTextBox = createTextBox(ArtistStrings.TracksIndexFormArtist, !string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist), - RomanisedArtistTextBox = createTextBox("Romanised Artist", + RomanisedArtistTextBox = createTextBox(EditorSetupMetadataStrings.RomanisedArtist, !string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)), Empty(), - TitleTextBox = createTextBox("Title", + TitleTextBox = createTextBox(BeatmapsetWatchesStrings.IndexTableTitle, !string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title), - RomanisedTitleTextBox = createTextBox("Romanised Title", + RomanisedTitleTextBox = createTextBox(EditorSetupMetadataStrings.RomanisedTitle, !string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)), Empty(), - creatorTextBox = createTextBox("Creator", metadata.Author.Username), - difficultyTextBox = createTextBox("Difficulty Name", Beatmap.BeatmapInfo.DifficultyName), + creatorTextBox = createTextBox(EditorSetupMetadataStrings.Creator, metadata.Author.Username), + difficultyTextBox = createTextBox(EditorSetupMetadataStrings.DifficultyName, Beatmap.BeatmapInfo.DifficultyName), sourceTextBox = createTextBox(BeatmapsetsStrings.ShowInfoSource, metadata.Source), tagsTextBox = createTextBox(BeatmapsetsStrings.ShowInfoTags, metadata.Tags) }; diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 8c14feebbc..dfc849de7b 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Overlays; +using osu.Game.Localisation; namespace osu.Game.Screens.Edit.Setup { @@ -19,7 +20,7 @@ namespace osu.Game.Screens.Edit.Setup private LabelledFileChooser audioTrackChooser; private LabelledFileChooser backgroundChooser; - public override LocalisableString Title => "Resources"; + public override LocalisableString Title => EditorSetupResourcesStrings.Resources; [Resolved] private MusicController music { get; set; } @@ -43,13 +44,13 @@ namespace osu.Game.Screens.Edit.Setup { backgroundChooser = new LabelledFileChooser(".jpg", ".jpeg", ".png") { - Label = "Background", + Label = GameplaySettingsStrings.BackgroundHeader, FixedLabelWidth = LABEL_WIDTH, TabbableContentContainer = this }, audioTrackChooser = new LabelledFileChooser(".mp3", ".ogg") { - Label = "Audio Track", + Label = EditorSetupResourcesStrings.AudioTrack, FixedLabelWidth = LABEL_WIDTH, TabbableContentContainer = this }, @@ -144,12 +145,12 @@ namespace osu.Game.Screens.Edit.Setup private void updatePlaceholderText() { audioTrackChooser.Text = audioTrackChooser.Current.Value == null - ? "Click to select a track" - : "Click to replace the track"; + ? EditorSetupResourcesStrings.ClickToSelectTrack + : EditorSetupResourcesStrings.ClickToReplaceTrack; backgroundChooser.Text = backgroundChooser.Current.Value == null - ? "Click to select a background image" - : "Click to replace the background image"; + ? EditorSetupResourcesStrings.ClickToSelectBackground + : EditorSetupResourcesStrings.ClickToReplaceBackground; } } } diff --git a/osu.Game/Screens/Edit/Setup/RulesetSetupSection.cs b/osu.Game/Screens/Edit/Setup/RulesetSetupSection.cs index db0641feba..6b1b1128d4 100644 --- a/osu.Game/Screens/Edit/Setup/RulesetSetupSection.cs +++ b/osu.Game/Screens/Edit/Setup/RulesetSetupSection.cs @@ -5,12 +5,13 @@ using osu.Framework.Localisation; using osu.Game.Rulesets; +using osu.Game.Localisation; namespace osu.Game.Screens.Edit.Setup { public abstract class RulesetSetupSection : SetupSection { - public sealed override LocalisableString Title => $"Ruleset ({rulesetInfo.Name})"; + public sealed override LocalisableString Title => EditorSetupRulesetStrings.Ruleset(rulesetInfo.Name); private readonly RulesetInfo rulesetInfo; diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs index c531f1da90..c1f6ab556c 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osuTK.Graphics; +using osu.Game.Localisation; namespace osu.Game.Screens.Edit.Setup { @@ -77,8 +78,8 @@ namespace osu.Game.Screens.Edit.Setup { public SetupScreenTitle() { - Title = "beatmap setup"; - Description = "change general settings of your beatmap"; + Title = EditorSetupStrings.BeatmapSetup; + Description = EditorSetupStrings.BeatmapSetupDescription; IconTexture = "Icons/Hexacons/social"; } } From 8cb2e11766b85d23ceb69b17275025090eb94328 Mon Sep 17 00:00:00 2001 From: naoei Date: Wed, 10 Aug 2022 15:51:11 -0400 Subject: [PATCH 099/532] Change most ruleset-accessible string types to Localisable strings --- osu.Game/Rulesets/Mods/IMod.cs | 3 ++- osu.Game/Rulesets/Mods/Mod.cs | 3 ++- osu.Game/Rulesets/Ruleset.cs | 5 +++-- osu.Game/Scoring/HitResultDisplayStatistic.cs | 5 +++-- osu.Game/Screens/Play/ResumeOverlay.cs | 3 ++- osu.Game/Screens/Ranking/Statistics/StatisticItem.cs | 5 +++-- 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index 30fa1ea8cb..1f9e26c9d7 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; namespace osu.Game.Rulesets.Mods { @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Mods /// /// The user readable description of this mod. /// - string Description { get; } + LocalisableString Description { get; } /// /// The type of this mod. diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 0f3d758f74..e4c91d3037 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -9,6 +9,7 @@ using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets.UI; @@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Mods public virtual ModType Type => ModType.Fun; [JsonIgnore] - public abstract string Description { get; } + public abstract LocalisableString Description { get; } /// /// The tooltip to display for this mod when used in a . diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index c1ec6c30ef..50ce6b3b12 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -28,6 +28,7 @@ using osu.Game.Users; using JetBrains.Annotations; using osu.Framework.Extensions; using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Extensions; using osu.Game.Rulesets.Filter; @@ -313,7 +314,7 @@ namespace osu.Game.Rulesets /// /// All valid s along with a display-friendly name. /// - public IEnumerable<(HitResult result, string displayName)> GetHitResults() + public IEnumerable<(HitResult result, LocalisableString displayName)> GetHitResults() { var validResults = GetValidHitResults(); @@ -351,7 +352,7 @@ namespace osu.Game.Rulesets /// /// The result type to get the name for. /// The display name. - public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription(); + public virtual LocalisableString GetDisplayNameForHitResult(HitResult result) => result.GetLocalisableDescription(); /// /// Creates ruleset-specific beatmap filter criteria to be used on the song select screen. diff --git a/osu.Game/Scoring/HitResultDisplayStatistic.cs b/osu.Game/Scoring/HitResultDisplayStatistic.cs index 4603ff053e..20deff4875 100644 --- a/osu.Game/Scoring/HitResultDisplayStatistic.cs +++ b/osu.Game/Scoring/HitResultDisplayStatistic.cs @@ -3,6 +3,7 @@ #nullable disable +using osu.Framework.Localisation; using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring @@ -30,9 +31,9 @@ namespace osu.Game.Scoring /// /// A custom display name for the result type. May be provided by rulesets to give better clarity. /// - public string DisplayName { get; } + public LocalisableString DisplayName { get; } - public HitResultDisplayStatistic(HitResult result, int count, int? maxCount, string displayName) + public HitResultDisplayStatistic(HitResult result, int count, int? maxCount, LocalisableString displayName) { Result = result; Count = count; diff --git a/osu.Game/Screens/Play/ResumeOverlay.cs b/osu.Game/Screens/Play/ResumeOverlay.cs index 2be1f93f80..7ed95c4ce3 100644 --- a/osu.Game/Screens/Play/ResumeOverlay.cs +++ b/osu.Game/Screens/Play/ResumeOverlay.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; 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 osuTK; @@ -31,7 +32,7 @@ namespace osu.Game.Screens.Play protected const float TRANSITION_TIME = 500; - protected abstract string Message { get; } + protected abstract LocalisableString Message { get; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index 0e5cce59f8..ae5d81c696 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -7,6 +7,7 @@ using System; using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; namespace osu.Game.Screens.Ranking.Statistics { @@ -18,7 +19,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// The name of this item. /// - public readonly string Name; + public readonly LocalisableString Name; /// /// A function returning the content to be displayed. @@ -48,7 +49,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// A function returning the content to be displayed. /// Whether this item requires hit events. If true, will not be called if no hit events are available. /// The of this item. This can be thought of as the column dimension of an encompassing . - public StatisticItem([NotNull] string name, [NotNull] Func createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null) + public StatisticItem(LocalisableString name, [NotNull] Func createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null) { Name = name; RequiresHitEvents = requiresHitEvents; From 60dae70a180f33c6c55be46018e43bad8ba627ea Mon Sep 17 00:00:00 2001 From: naoei Date: Wed, 10 Aug 2022 15:54:48 -0400 Subject: [PATCH 100/532] Change mod description type to `LocalisableString` --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 3 ++- osu.Game/Rulesets/Mods/ModAutoplay.cs | 3 ++- osu.Game/Rulesets/Mods/ModBarrelRoll.cs | 3 ++- osu.Game/Rulesets/Mods/ModCinema.cs | 3 ++- osu.Game/Rulesets/Mods/ModClassic.cs | 3 ++- osu.Game/Rulesets/Mods/ModDaycore.cs | 3 ++- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 3 ++- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 3 ++- osu.Game/Rulesets/Mods/ModFlashlight.cs | 3 ++- osu.Game/Rulesets/Mods/ModHalfTime.cs | 3 ++- osu.Game/Rulesets/Mods/ModHardRock.cs | 3 ++- osu.Game/Rulesets/Mods/ModMuted.cs | 2 +- osu.Game/Rulesets/Mods/ModNightcore.cs | 3 ++- osu.Game/Rulesets/Mods/ModNoFail.cs | 3 ++- osu.Game/Rulesets/Mods/ModNoMod.cs | 3 ++- osu.Game/Rulesets/Mods/ModPerfect.cs | 3 ++- osu.Game/Rulesets/Mods/ModSuddenDeath.cs | 3 ++- osu.Game/Rulesets/Mods/ModWindDown.cs | 3 ++- osu.Game/Rulesets/Mods/ModWindUp.cs | 3 ++- osu.Game/Rulesets/Mods/MultiMod.cs | 3 ++- osu.Game/Rulesets/Mods/UnknownMod.cs | 4 +++- 21 files changed, 42 insertions(+), 21 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 38d26ed05a..e7996c6d43 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "AS"; - public override string Description => "Let track speed adapt to you."; + public override LocalisableString Description => "Let track speed adapt to you."; public override ModType Type => ModType.Fun; diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index ab49dd5575..6cafe0716d 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Replays; @@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "AT"; public override IconUsage? Icon => OsuIcon.ModAuto; public override ModType Type => ModType.Automation; - public override string Description => "Watch a perfect automated play through the song."; + public override LocalisableString Description => "Watch a perfect automated play through the song."; public override double ScoreMultiplier => 1; public bool PerformFail() => false; diff --git a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs index bacb953f76..0c301d293f 100644 --- a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs +++ b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; @@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Barrel Roll"; public override string Acronym => "BR"; - public override string Description => "The whole playfield is on a wheel!"; + public override LocalisableString Description => "The whole playfield is on a wheel!"; public override double ScoreMultiplier => 1; public override string SettingDescription => $"{SpinSpeed.Value:N2} rpm {Direction.Value.GetDescription().ToLowerInvariant()}"; diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 99c4e71d1f..ae661c5f25 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; @@ -27,7 +28,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Cinema"; public override string Acronym => "CN"; public override IconUsage? Icon => OsuIcon.ModCinema; - public override string Description => "Watch the video without visual distractions."; + public override LocalisableString Description => "Watch the video without visual distractions."; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAutoplay)).ToArray(); diff --git a/osu.Game/Rulesets/Mods/ModClassic.cs b/osu.Game/Rulesets/Mods/ModClassic.cs index 1159955e11..55b16297e2 100644 --- a/osu.Game/Rulesets/Mods/ModClassic.cs +++ b/osu.Game/Rulesets/Mods/ModClassic.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; namespace osu.Game.Rulesets.Mods { @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => FontAwesome.Solid.History; - public override string Description => "Feeling nostalgic?"; + public override LocalisableString Description => "Feeling nostalgic?"; public override ModType Type => ModType.Conversion; } diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs index 9e8e44229e..de1a5ab56c 100644 --- a/osu.Game/Rulesets/Mods/ModDaycore.cs +++ b/osu.Game/Rulesets/Mods/ModDaycore.cs @@ -4,6 +4,7 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; namespace osu.Game.Rulesets.Mods { @@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Daycore"; public override string Acronym => "DC"; public override IconUsage? Icon => null; - public override string Description => "Whoaaaaa..."; + public override LocalisableString Description => "Whoaaaaa..."; private readonly BindableNumber tempoAdjust = new BindableDouble(1); private readonly BindableNumber freqAdjust = new BindableDouble(1); diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index eefa1531c4..4ed31eec78 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods { public override string Name => @"Difficulty Adjust"; - public override string Description => @"Override a beatmap's difficulty settings."; + public override LocalisableString Description => @"Override a beatmap's difficulty settings."; public override string Acronym => "DA"; diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 1c71f5d055..d8a41ae658 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -3,6 +3,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics; @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "DT"; public override IconUsage? Icon => OsuIcon.ModDoubleTime; public override ModType Type => ModType.DifficultyIncrease; - public override string Description => "Zoooooooooom..."; + public override LocalisableString Description => "Zoooooooooom..."; [SettingSource("Speed increase", "The actual increase to apply")] public override BindableNumber SpeedChange { get; } = new BindableDouble diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 210dd56137..558605efc3 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Beatmaps.Timing; using osu.Game.Configuration; using osu.Game.Graphics; @@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "FL"; public override IconUsage? Icon => OsuIcon.ModFlashlight; public override ModType Type => ModType.DifficultyIncrease; - public override string Description => "Restricted view area."; + public override LocalisableString Description => "Restricted view area."; [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] public abstract BindableFloat SizeMultiplier { get; } diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 13d89e30d6..8d8b97e79e 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -3,6 +3,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics; @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "HT"; public override IconUsage? Icon => OsuIcon.ModHalftime; public override ModType Type => ModType.DifficultyReduction; - public override string Description => "Less zoom..."; + public override LocalisableString Description => "Less zoom..."; [SettingSource("Speed decrease", "The actual decrease to apply")] public override BindableNumber SpeedChange { get; } = new BindableDouble diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index 0a5348a8cf..2886e59c54 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "HR"; public override IconUsage? Icon => OsuIcon.ModHardRock; public override ModType Type => ModType.DifficultyIncrease; - public override string Description => "Everything just got a bit harder..."; + public override LocalisableString Description => "Everything just got a bit harder..."; public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) }; public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) diff --git a/osu.Game/Rulesets/Mods/ModMuted.cs b/osu.Game/Rulesets/Mods/ModMuted.cs index 55d5abfa82..9735d6b536 100644 --- a/osu.Game/Rulesets/Mods/ModMuted.cs +++ b/osu.Game/Rulesets/Mods/ModMuted.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Muted"; public override string Acronym => "MU"; public override IconUsage? Icon => FontAwesome.Solid.VolumeMute; - public override string Description => "Can you still feel the rhythm without music?"; + public override LocalisableString Description => "Can you still feel the rhythm without music?"; public override ModType Type => ModType.Fun; public override double ScoreMultiplier => 1; } diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index c4417ec509..099bf386f3 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -7,6 +7,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; @@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Nightcore"; public override string Acronym => "NC"; public override IconUsage? Icon => OsuIcon.ModNightcore; - public override string Description => "Uguuuuuuuu..."; + public override LocalisableString Description => "Uguuuuuuuu..."; } public abstract class ModNightcore : ModNightcore, IApplicableToDrawableRuleset diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs index 5ebae17228..31bb4338b3 100644 --- a/osu.Game/Rulesets/Mods/ModNoFail.cs +++ b/osu.Game/Rulesets/Mods/ModNoFail.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Graphics; namespace osu.Game.Rulesets.Mods @@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "NF"; public override IconUsage? Icon => OsuIcon.ModNoFail; public override ModType Type => ModType.DifficultyReduction; - public override string Description => "You can't fail, no matter what."; + public override LocalisableString Description => "You can't fail, no matter what."; public override double ScoreMultiplier => 0.5; public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModAutoplay) }; } diff --git a/osu.Game/Rulesets/Mods/ModNoMod.cs b/osu.Game/Rulesets/Mods/ModNoMod.cs index 1009c5bc42..5dd4b317e7 100644 --- a/osu.Game/Rulesets/Mods/ModNoMod.cs +++ b/osu.Game/Rulesets/Mods/ModNoMod.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; namespace osu.Game.Rulesets.Mods { @@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mods { public override string Name => "No Mod"; public override string Acronym => "NM"; - public override string Description => "No mods applied."; + public override LocalisableString Description => "No mods applied."; public override double ScoreMultiplier => 1; public override IconUsage? Icon => FontAwesome.Solid.Ban; public override ModType Type => ModType.System; diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 9016a24f8d..804f23b6b7 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.ModPerfect; public override ModType Type => ModType.DifficultyIncrease; public override double ScoreMultiplier => 1; - public override string Description => "SS or quit."; + public override LocalisableString Description => "SS or quit."; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModSuddenDeath)).ToArray(); diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index c8b835f78a..4e4e8662e8 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "SD"; public override IconUsage? Icon => OsuIcon.ModSuddenDeath; public override ModType Type => ModType.DifficultyIncrease; - public override string Description => "Miss and fail."; + public override LocalisableString Description => "Miss and fail."; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray(); diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs index 08bd44f7bd..e84bdab69c 100644 --- a/osu.Game/Rulesets/Mods/ModWindDown.cs +++ b/osu.Game/Rulesets/Mods/ModWindDown.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Configuration; namespace osu.Game.Rulesets.Mods @@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mods { public override string Name => "Wind Down"; public override string Acronym => "WD"; - public override string Description => "Sloooow doooown..."; + public override LocalisableString Description => "Sloooow doooown..."; public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleDown; public override double ScoreMultiplier => 1.0; diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs index df8f781148..39cee50f96 100644 --- a/osu.Game/Rulesets/Mods/ModWindUp.cs +++ b/osu.Game/Rulesets/Mods/ModWindUp.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Configuration; namespace osu.Game.Rulesets.Mods @@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mods { public override string Name => "Wind Up"; public override string Acronym => "WU"; - public override string Description => "Can you keep up?"; + public override LocalisableString Description => "Can you keep up?"; public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleUp; public override double ScoreMultiplier => 1.0; diff --git a/osu.Game/Rulesets/Mods/MultiMod.cs b/osu.Game/Rulesets/Mods/MultiMod.cs index 1c41c6b8b3..9fbc0ddd97 100644 --- a/osu.Game/Rulesets/Mods/MultiMod.cs +++ b/osu.Game/Rulesets/Mods/MultiMod.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Localisation; namespace osu.Game.Rulesets.Mods { @@ -10,7 +11,7 @@ namespace osu.Game.Rulesets.Mods { public override string Name => string.Empty; public override string Acronym => string.Empty; - public override string Description => string.Empty; + public override LocalisableString Description => string.Empty; public override double ScoreMultiplier => 0; public Mod[] Mods { get; } diff --git a/osu.Game/Rulesets/Mods/UnknownMod.cs b/osu.Game/Rulesets/Mods/UnknownMod.cs index 72de0ad653..abe05996ff 100644 --- a/osu.Game/Rulesets/Mods/UnknownMod.cs +++ b/osu.Game/Rulesets/Mods/UnknownMod.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; + namespace osu.Game.Rulesets.Mods { public class UnknownMod : Mod @@ -12,7 +14,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => $"Unknown mod ({OriginalAcronym})"; public override string Acronym => $"{OriginalAcronym}??"; - public override string Description => "This mod could not be resolved by the game."; + public override LocalisableString Description => "This mod could not be resolved by the game."; public override double ScoreMultiplier => 0; public override bool UserPlayable => false; From 1e356f61376984f11c23c5b03da8a3fa8a70c41b Mon Sep 17 00:00:00 2001 From: naoei Date: Wed, 10 Aug 2022 16:03:59 -0400 Subject: [PATCH 101/532] Revert localisation for `GetDisplayNameForHitResult` Came across an issue where `LeaderboardScoreTooltip` attempts to capitalize all letters for the `displayName`. Unsure if I should completely ignore it and localise it anyway. --- osu.Game/Rulesets/Ruleset.cs | 5 ++--- osu.Game/Scoring/HitResultDisplayStatistic.cs | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 50ce6b3b12..c1ec6c30ef 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -28,7 +28,6 @@ using osu.Game.Users; using JetBrains.Annotations; using osu.Framework.Extensions; using osu.Framework.Extensions.EnumExtensions; -using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Extensions; using osu.Game.Rulesets.Filter; @@ -314,7 +313,7 @@ namespace osu.Game.Rulesets /// /// All valid s along with a display-friendly name. /// - public IEnumerable<(HitResult result, LocalisableString displayName)> GetHitResults() + public IEnumerable<(HitResult result, string displayName)> GetHitResults() { var validResults = GetValidHitResults(); @@ -352,7 +351,7 @@ namespace osu.Game.Rulesets /// /// The result type to get the name for. /// The display name. - public virtual LocalisableString GetDisplayNameForHitResult(HitResult result) => result.GetLocalisableDescription(); + public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription(); /// /// Creates ruleset-specific beatmap filter criteria to be used on the song select screen. diff --git a/osu.Game/Scoring/HitResultDisplayStatistic.cs b/osu.Game/Scoring/HitResultDisplayStatistic.cs index 20deff4875..4603ff053e 100644 --- a/osu.Game/Scoring/HitResultDisplayStatistic.cs +++ b/osu.Game/Scoring/HitResultDisplayStatistic.cs @@ -3,7 +3,6 @@ #nullable disable -using osu.Framework.Localisation; using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring @@ -31,9 +30,9 @@ namespace osu.Game.Scoring /// /// A custom display name for the result type. May be provided by rulesets to give better clarity. /// - public LocalisableString DisplayName { get; } + public string DisplayName { get; } - public HitResultDisplayStatistic(HitResult result, int count, int? maxCount, LocalisableString displayName) + public HitResultDisplayStatistic(HitResult result, int count, int? maxCount, string displayName) { Result = result; Count = count; From 6e13cf82e810504f8f7a3df4bd341c42853d6d69 Mon Sep 17 00:00:00 2001 From: naoei Date: Wed, 10 Aug 2022 16:05:34 -0400 Subject: [PATCH 102/532] Don't render statistic header if display string is null --- osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs | 4 ++-- osu.Game/Screens/Ranking/Statistics/StatisticItem.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index 078ca97737..1cf46dcf04 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Ranking.Statistics private static Drawable createHeader(StatisticItem item) { - if (string.IsNullOrEmpty(item.Name)) + if (item.Name == null) return Empty(); return new FillFlowContainer @@ -82,7 +82,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = item.Name, + Text = item.Name.Value, Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index ae5d81c696..baabc90c6f 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// The name of this item. /// - public readonly LocalisableString Name; + public readonly LocalisableString? Name; /// /// A function returning the content to be displayed. From 3e38baca3c0280ecfb9bc9ef7f3c5c185ba9f0ca Mon Sep 17 00:00:00 2001 From: naoei Date: Wed, 10 Aug 2022 16:09:11 -0400 Subject: [PATCH 103/532] Change ruleset mod description types --- osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs | 3 ++- .../Mods/CatchModFloatingFruits.cs | 3 ++- osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs | 3 ++- osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs | 3 ++- osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs | 3 ++- osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs | 3 ++- osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs | 3 ++- osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs | 3 ++- osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs | 3 ++- osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs | 3 ++- osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs | 3 ++- osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs | 3 ++- osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs | 3 ++- osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs | 4 +++- osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs | 4 +++- osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs | 4 +++- osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs | 4 +++- osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs | 4 +++- osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs | 4 +++- osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs | 4 +++- osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs | 4 +++- osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs | 4 +++- osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs | 4 +++- osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs | 3 ++- osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs | 3 ++- osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs | 3 ++- osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs | 3 ++- osu.Game.Rulesets.Taiko/Mods/TaikoModRandom.cs | 3 ++- osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs | 3 ++- osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs | 3 ++- osu.Game.Tests/Mods/ModUtilsTest.cs | 5 +++-- osu.Game.Tests/Mods/TestCustomisableModRuleset.cs | 3 ++- .../DifficultyAdjustmentModCombinationsTest.cs | 11 ++++++----- osu.Game.Tests/Online/TestAPIModJsonSerialization.cs | 5 +++-- .../Online/TestAPIModMessagePackSerialization.cs | 7 ++++--- .../Visual/Gameplay/TestScenePlayerLoader.cs | 3 ++- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 3 ++- 60 files changed, 138 insertions(+), 68 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs index 16ef56d845..cac5b9aa6a 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs @@ -1,12 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Catch.Mods { public class CatchModEasy : ModEasyWithExtraLives { - public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!"; + public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!"; } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs index 63203dd57c..e12181d051 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public override string Name => "Floating Fruits"; public override string Acronym => "FF"; - public override string Description => "The fruits are... floating?"; + public override LocalisableString Description => "The fruits are... floating?"; public override double ScoreMultiplier => 1; public override IconUsage? Icon => FontAwesome.Solid.Cloud; diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs index 51516edacd..d68430b64f 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs @@ -3,6 +3,7 @@ using System.Linq; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.UI; @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset { - public override string Description => @"Play with fading fruits."; + public override LocalisableString Description => @"Play with fading fruits."; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1; private const double fade_out_offset_multiplier = 0.6; diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs b/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs index a97e940a64..4cd2efdc2f 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Objects; @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModMirror : ModMirror, IApplicableToBeatmap { - public override string Description => "Fruits are flipped horizontally."; + public override LocalisableString Description => "Fruits are flipped horizontally."; /// /// is used instead of , diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs index a24a6227fe..9038153e20 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; using osu.Framework.Utils; using osu.Game.Configuration; @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModNoScope : ModNoScope, IUpdatableByPlayfield { - public override string Description => "Where's the catcher?"; + public override LocalisableString Description => "Where's the catcher?"; [SettingSource( "Hidden at combo", diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs index 60f1614d98..69ae8328e9 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Mods; @@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset, IApplicableToPlayer { - public override string Description => @"Use the mouse to control the catcher."; + public override LocalisableString Description => @"Use the mouse to control the catcher."; private DrawableRuleset drawableRuleset = null!; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs index 614ef76a3b..0d0c0d8f68 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override double ScoreMultiplier => 1; - public override string Description => "No more tricky speed changes!"; + public override LocalisableString Description => "No more tricky speed changes!"; public override IconUsage? Icon => FontAwesome.Solid.Equals; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs index c78bf72979..2457aa75d7 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mods; @@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public override string Name => "Dual Stages"; public override string Acronym => "DS"; - public override string Description => @"Double the stages, double the fun!"; + public override LocalisableString Description => @"Double the stages, double the fun!"; public override ModType Type => ModType.Conversion; public override double ScoreMultiplier => 1; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs index 4093aeb2a7..5c8cd6a5ae 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs @@ -1,12 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModEasy : ModEasyWithExtraLives { - public override string Description => @"More forgiving HP drain, less accuracy required, and three lives!"; + public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and three lives!"; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index f80c9e1f7c..c6e9c339f4 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Localisation; using osu.Game.Rulesets.Mania.UI; namespace osu.Game.Rulesets.Mania.Mods @@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public override string Name => "Fade In"; public override string Acronym => "FI"; - public override string Description => @"Keys appear out of nowhere!"; + public override LocalisableString Description => @"Keys appear out of nowhere!"; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray(); diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index e3ac624a6e..eeb6e94fc7 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -3,13 +3,14 @@ using System; using System.Linq; +using osu.Framework.Localisation; using osu.Game.Rulesets.Mania.UI; namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModHidden : ManiaModPlayfieldCover { - public override string Description => @"Keys fade out before you hit them!"; + public override LocalisableString Description => @"Keys fade out before you hit them!"; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray(); diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs index a65938184c..ca9bc89473 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; using osu.Framework.Graphics.Sprites; using System.Collections.Generic; +using osu.Framework.Localisation; using osu.Game.Rulesets.Mania.Beatmaps; namespace osu.Game.Rulesets.Mania.Mods @@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override double ScoreMultiplier => 1; - public override string Description => @"Replaces all hold notes with normal notes."; + public override LocalisableString Description => @"Replaces all hold notes with normal notes."; public override IconUsage? Icon => FontAwesome.Solid.DotCircle; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs index 4cbdaee323..ef9154d180 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; @@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Acronym => "IN"; public override double ScoreMultiplier => 1; - public override string Description => "Hold the keys. To the beat."; + public override LocalisableString Description => "Hold the keys. To the beat."; public override IconUsage? Icon => FontAwesome.Solid.YinYang; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs index 948979505c..31f52610e9 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; + namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModKey1 : ManiaKeyMod @@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 1; public override string Name => "One Key"; public override string Acronym => "1K"; - public override string Description => @"Play with one key."; + public override LocalisableString Description => @"Play with one key."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs index 684370fc3d..67e65b887a 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; + namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModKey10 : ManiaKeyMod @@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 10; public override string Name => "Ten Keys"; public override string Acronym => "10K"; - public override string Description => @"Play with ten keys."; + public override LocalisableString Description => @"Play with ten keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs index de91902ca8..0f8148d252 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; + namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModKey2 : ManiaKeyMod @@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 2; public override string Name => "Two Keys"; public override string Acronym => "2K"; - public override string Description => @"Play with two keys."; + public override LocalisableString Description => @"Play with two keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs index 8575a96bde..0f8af7940c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; + namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModKey3 : ManiaKeyMod @@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 3; public override string Name => "Three Keys"; public override string Acronym => "3K"; - public override string Description => @"Play with three keys."; + public override LocalisableString Description => @"Play with three keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs index 54ea3afa07..d3a4546dce 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; + namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModKey4 : ManiaKeyMod @@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 4; public override string Name => "Four Keys"; public override string Acronym => "4K"; - public override string Description => @"Play with four keys."; + public override LocalisableString Description => @"Play with four keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs index e9a9bba5bd..693182a952 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; + namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModKey5 : ManiaKeyMod @@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 5; public override string Name => "Five Keys"; public override string Acronym => "5K"; - public override string Description => @"Play with five keys."; + public override LocalisableString Description => @"Play with five keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs index b9606d1cb5..ab911292f7 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; + namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModKey6 : ManiaKeyMod @@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 6; public override string Name => "Six Keys"; public override string Acronym => "6K"; - public override string Description => @"Play with six keys."; + public override LocalisableString Description => @"Play with six keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs index b80d794085..ab401ef1d0 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; + namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModKey7 : ManiaKeyMod @@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 7; public override string Name => "Seven Keys"; public override string Acronym => "7K"; - public override string Description => @"Play with seven keys."; + public override LocalisableString Description => @"Play with seven keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs index 3462d634a4..b3e8a45dda 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; + namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModKey8 : ManiaKeyMod @@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 8; public override string Name => "Eight Keys"; public override string Acronym => "8K"; - public override string Description => @"Play with eight keys."; + public override LocalisableString Description => @"Play with eight keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs index 83c505c048..5972cbf0fe 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; + namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModKey9 : ManiaKeyMod @@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override int KeyCount => 9; public override string Name => "Nine Keys"; public override string Acronym => "9K"; - public override string Description => @"Play with nine keys."; + public override LocalisableString Description => @"Play with nine keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs index 9c3744ea98..f9690b4298 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs @@ -5,6 +5,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; using System.Linq; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; @@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModMirror : ModMirror, IApplicableToBeatmap { - public override string Description => "Notes are flipped horizontally."; + public override LocalisableString Description => "Notes are flipped horizontally."; public void ApplyToBeatmap(IBeatmap beatmap) { diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs index dfb02408d2..6ff070d703 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModRandom : ModRandom, IApplicableToBeatmap { - public override string Description => @"Shuffle around the keys!"; + public override LocalisableString Description => @"Shuffle around the keys!"; public void ApplyToBeatmap(IBeatmap beatmap) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index d88cb17e84..9bf5d33d4a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; namespace osu.Game.Rulesets.Osu.Mods { @@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => @"Alternate"; public override string Acronym => @"AL"; - public override string Description => @"Don't use the same key twice in a row!"; + public override LocalisableString Description => @"Don't use the same key twice in a row!"; public override IconUsage? Icon => FontAwesome.Solid.Keyboard; public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray(); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs index e6889403a3..ec93f19e17 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; @@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => "Approach Different"; public override string Acronym => "AD"; - public override string Description => "Never trust the approach circles..."; + public override LocalisableString Description => "Never trust the approach circles..."; public override double ScoreMultiplier => 1; public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 872fcf7e9b..a42eaec96b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.StateChanges; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; @@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => "AP"; public override IconUsage? Icon => OsuIcon.ModAutopilot; public override ModType Type => ModType.Automation; - public override string Description => @"Automatic cursor movement - just follow the rhythm."; + public override LocalisableString 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(OsuModMagnetised), typeof(OsuModRepel) }; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs index 56665db770..4c72667f15 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods public class OsuModBlinds : Mod, IApplicableToDrawableRuleset, IApplicableToHealthProcessor { public override string Name => "Blinds"; - public override string Description => "Play with blinds on your screen."; + public override LocalisableString Description => "Play with blinds on your screen."; public override string Acronym => "BL"; public override IconUsage? Icon => FontAwesome.Solid.Adjust; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs index ee6a7815e2..e624660410 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs @@ -3,6 +3,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Configuration; namespace osu.Game.Rulesets.Osu.Mods @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override IconUsage? Icon => FontAwesome.Solid.CompressArrowsAlt; - public override string Description => "Hit them at the right size!"; + public override LocalisableString Description => "Hit them at the right size!"; [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")] public override BindableNumber StartScale { get; } = new BindableFloat diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs index 06b5b6cfb8..281b36e70e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs @@ -1,12 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Osu.Mods { public class OsuModEasy : ModEasyWithExtraLives { - public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!"; + public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!"; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs index 182d6eeb4b..b77c887cd3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs @@ -3,6 +3,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Configuration; namespace osu.Game.Rulesets.Osu.Mods @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override IconUsage? Icon => FontAwesome.Solid.ArrowsAltV; - public override string Description => "Hit them at the right size!"; + public override LocalisableString Description => "Hit them at the right size!"; [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")] public override BindableNumber StartScale { get; } = new BindableFloat diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 97f201b2cc..996ee1cddb 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Only fade approach circles", "The main object body will not fade when enabled.")] public Bindable OnlyFadeApproachCircles { get; } = new BindableBool(); - public override string Description => @"Play with no approach circles and fading circles/sliders."; + public override LocalisableString Description => @"Play with no approach circles and fading circles/sliders."; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1; public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) }; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index 9316f9ed74..6871c26750 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; @@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods 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 circles – your cursor is a magnet!"; + public override LocalisableString 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), typeof(OsuModRepel) }; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs index 3faca0b01f..0a54d58718 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModMirror : ModMirror, IApplicableToHitObject { - public override string Description => "Flip objects on the chosen axes."; + public override LocalisableString Description => "Flip objects on the chosen axes."; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) }; [SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")] diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs index 3eb8982f5d..817f7b599c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModNoScope : ModNoScope, IUpdatableByPlayfield, IApplicableToBeatmap { - public override string Description => "Where's the cursor?"; + public override LocalisableString Description => "Where's the cursor?"; private PeriodTracker spinnerPeriods = null!; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 4f83154728..96c02a508b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// public class OsuModRandom : ModRandom, IApplicableToBeatmap { - public override string Description => "It never gets boring!"; + public override LocalisableString Description => "It never gets boring!"; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray(); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 908bb34ed6..fac1cbfd47 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer { - public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; + public override LocalisableString 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(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray(); /// diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs index 211987ee32..54b594505c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; @@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Name => "Repel"; public override string Acronym => "RP"; public override ModType Type => ModType.Fun; - public override string Description => "Hit objects run away!"; + public override LocalisableString Description => "Hit objects run away!"; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) }; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs index b170d30448..91731b25cf 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Localisation; namespace osu.Game.Rulesets.Osu.Mods { @@ -10,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => @"Single Tap"; public override string Acronym => @"SG"; - public override string Description => @"You must only use one key!"; + public override LocalisableString Description => @"You must only use one key!"; public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray(); protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs index 95e7d13ee7..b0533d0cfa 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => "SI"; public override IconUsage? Icon => FontAwesome.Solid.Undo; public override ModType Type => ModType.Fun; - public override string Description => "Circles spin in. No approach circles."; + public override LocalisableString Description => "Circles spin in. No approach circles."; public override double ScoreMultiplier => 1; // todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque, diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index d9ab749ad3..9708800daa 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => "SO"; public override IconUsage? Icon => OsuIcon.ModSpunOut; public override ModType Type => ModType.Automation; - public override string Description => @"Spinners will be automatically completed."; + public override LocalisableString Description => @"Spinners will be automatically completed."; public override double ScoreMultiplier => 0.9; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTarget) }; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 0b34ab28a3..67b19124e1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Threading; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; @@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Name => @"Strict Tracking"; public override string Acronym => @"ST"; public override ModType Type => ModType.DifficultyIncrease; - public override string Description => @"Once you start a slider, follow precisely or get a miss."; + public override LocalisableString Description => @"Once you start a slider, follow precisely or get a miss."; public override double ScoreMultiplier => 1.0; public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) }; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 623157a427..82260db818 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => "TP"; public override ModType Type => ModType.Conversion; public override IconUsage? Icon => OsuIcon.ModTarget; - public override string Description => @"Practice keeping up with the beat of the song."; + public override LocalisableString Description => @"Practice keeping up with the beat of the song."; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs index 7276cc753c..fd5c46a226 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Osu.Mods @@ -9,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => "Touch Device"; public override string Acronym => "TD"; - public override string Description => "Automatically applied to plays on devices with a touchscreen."; + public override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen."; public override double ScoreMultiplier => 1; public override ModType Type => ModType.System; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index d862d36670..25d05a88a8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Name => "Traceable"; public override string Acronym => "TC"; public override ModType Type => ModType.Fun; - public override string Description => "Put your faith in the approach circles..."; + public override LocalisableString Description => "Put your faith in the approach circles..."; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) }; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs index 4354ecbe9a..2354cd50ae 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => "TR"; public override IconUsage? Icon => FontAwesome.Solid.ArrowsAlt; public override ModType Type => ModType.Fun; - public override string Description => "Everything rotates. EVERYTHING."; + public override LocalisableString Description => "Everything rotates. EVERYTHING."; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) }; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs index 3f1c3aa812..a45338d91f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; @@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => "WG"; public override IconUsage? Icon => FontAwesome.Solid.Certificate; public override ModType Type => ModType.Fun; - public override string Description => "They just won't stay still..."; + public override LocalisableString Description => "They just won't stay still..."; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel) }; diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs index ad6fdf59e2..009f2854f8 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -8,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModEasy : ModEasy { - public override string Description => @"Beats move slower, and less accuracy required!"; + public override LocalisableString Description => @"Beats move slower, and less accuracy required!"; /// /// Multiplier factor added to the scrolling speed. diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 4c802978e3..4708ef9bf0 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModHidden : ModHidden, IApplicableToDrawableRuleset { - public override string Description => @"Beats fade out before you hit them!"; + public override LocalisableString Description => @"Beats fade out before you hit them!"; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1; /// diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModRandom.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModRandom.cs index 307a37bf2e..c0be0290e6 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModRandom.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModRandom.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModRandom : ModRandom, IApplicableToBeatmap { - public override string Description => @"Shuffle around the colours!"; + public override LocalisableString Description => @"Shuffle around the colours!"; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(TaikoModSwap)).ToArray(); public void ApplyToBeatmap(IBeatmap beatmap) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs index 7be70d9ac3..d1e9ab1428 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs @@ -1,12 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModRelax : ModRelax { - public override string Description => @"No ninja-like spinners, demanding drumrolls or unexpected katu's."; + public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katu's."; } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs index 3cb337c41d..fc3913f56d 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Beatmaps; @@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override string Name => "Swap"; public override string Acronym => "SW"; - public override string Description => @"Dons become kats, kats become dons"; + public override LocalisableString Description => @"Dons become kats, kats become dons"; public override ModType Type => ModType.Conversion; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModRandom)).ToArray(); diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 3b391f6756..aa41fd830b 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using Moq; using NUnit.Framework; +using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Utils; @@ -320,7 +321,7 @@ namespace osu.Game.Tests.Mods public class InvalidMultiplayerMod : Mod { public override string Name => string.Empty; - public override string Description => string.Empty; + public override LocalisableString Description => string.Empty; public override string Acronym => string.Empty; public override double ScoreMultiplier => 1; public override bool HasImplementation => true; @@ -331,7 +332,7 @@ namespace osu.Game.Tests.Mods private class InvalidMultiplayerFreeMod : Mod { public override string Name => string.Empty; - public override string Description => string.Empty; + public override LocalisableString Description => string.Empty; public override string Acronym => string.Empty; public override double ScoreMultiplier => 1; public override bool HasImplementation => true; diff --git a/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs b/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs index 9e3354935a..7df5448ff7 100644 --- a/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs +++ b/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets; @@ -60,7 +61,7 @@ namespace osu.Game.Tests.Mods { public override double ScoreMultiplier => 1.0; - public override string Description => "This is a customisable test mod."; + public override LocalisableString Description => "This is a customisable test mod."; public override ModType Type => ModType.Conversion; diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 8d1c266473..6637d640b2 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; @@ -160,7 +161,7 @@ namespace osu.Game.Tests.NonVisual { public override string Name => nameof(ModA); public override string Acronym => nameof(ModA); - public override string Description => string.Empty; + public override LocalisableString Description => string.Empty; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB) }; @@ -169,7 +170,7 @@ namespace osu.Game.Tests.NonVisual private class ModB : Mod { public override string Name => nameof(ModB); - public override string Description => string.Empty; + public override LocalisableString Description => string.Empty; public override string Acronym => nameof(ModB); public override double ScoreMultiplier => 1; @@ -180,7 +181,7 @@ namespace osu.Game.Tests.NonVisual { public override string Name => nameof(ModC); public override string Acronym => nameof(ModC); - public override string Description => string.Empty; + public override LocalisableString Description => string.Empty; public override double ScoreMultiplier => 1; } @@ -188,7 +189,7 @@ namespace osu.Game.Tests.NonVisual { public override string Name => $"Incompatible With {nameof(ModA)}"; public override string Acronym => $"Incompatible With {nameof(ModA)}"; - public override string Description => string.Empty; + public override LocalisableString Description => string.Empty; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModA) }; @@ -207,7 +208,7 @@ namespace osu.Game.Tests.NonVisual { public override string Name => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}"; public override string Acronym => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}"; - public override string Description => string.Empty; + public override LocalisableString Description => string.Empty; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModA), typeof(ModB) }; diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs index 7458508c7a..17709fb10f 100644 --- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs @@ -9,6 +9,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NUnit.Framework; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Online.API; @@ -182,7 +183,7 @@ namespace osu.Game.Tests.Online { public override string Name => "Test Mod"; public override string Acronym => "TM"; - public override string Description => "This is a test mod."; + public override LocalisableString Description => "This is a test mod."; public override double ScoreMultiplier => 1; [SettingSource("Test")] @@ -199,7 +200,7 @@ namespace osu.Game.Tests.Online { public override string Name => "Test Mod"; public override string Acronym => "TMTR"; - public override string Description => "This is a test mod."; + public override LocalisableString Description => "This is a test mod."; public override double ScoreMultiplier => 1; [SettingSource("Initial rate", "The starting speed of the track")] diff --git a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs index a89d68bf15..b17414e026 100644 --- a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using MessagePack; using NUnit.Framework; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Online.API; @@ -102,7 +103,7 @@ namespace osu.Game.Tests.Online { public override string Name => "Test Mod"; public override string Acronym => "TM"; - public override string Description => "This is a test mod."; + public override LocalisableString Description => "This is a test mod."; public override double ScoreMultiplier => 1; [SettingSource("Test")] @@ -119,7 +120,7 @@ namespace osu.Game.Tests.Online { public override string Name => "Test Mod"; public override string Acronym => "TMTR"; - public override string Description => "This is a test mod."; + public override LocalisableString Description => "This is a test mod."; public override double ScoreMultiplier => 1; [SettingSource("Initial rate", "The starting speed of the track")] @@ -154,7 +155,7 @@ namespace osu.Game.Tests.Online { public override string Name => "Test Mod"; public override string Acronym => "TM"; - public override string Description => "This is a test mod."; + public override LocalisableString Description => "This is a test mod."; public override double ScoreMultiplier => 1; [SettingSource("Test")] diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 05474e3d39..94114e36f6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -13,6 +13,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Framework.Screens; using osu.Framework.Testing; @@ -374,7 +375,7 @@ namespace osu.Game.Tests.Visual.Gameplay public override string Name => string.Empty; public override string Acronym => string.Empty; public override double ScoreMultiplier => 1; - public override string Description => string.Empty; + public override LocalisableString Description => string.Empty; public bool Applied { get; private set; } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 07473aa55b..a821a4a6b9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -11,6 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; @@ -540,7 +541,7 @@ namespace osu.Game.Tests.Visual.UserInterface { public override string Name => "Unimplemented mod"; public override string Acronym => "UM"; - public override string Description => "A mod that is not implemented."; + public override LocalisableString Description => "A mod that is not implemented."; public override double ScoreMultiplier => 1; public override ModType Type => ModType.Conversion; } From a42b8092af4e27af1978928bd61bf0b1e24ba65d Mon Sep 17 00:00:00 2001 From: naoei Date: Wed, 10 Aug 2022 16:09:58 -0400 Subject: [PATCH 104/532] Change message type osu resume overlay --- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index fc3f89a836..412505331b 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play; using osuTK; @@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null; - protected override string Message => "Click the orange cursor to resume"; + protected override LocalisableString Message => "Click the orange cursor to resume"; [BackgroundDependencyLoader] private void load() From 7cbe2fa522a1591a9eab3307a20255d252ad5ba7 Mon Sep 17 00:00:00 2001 From: naoei Date: Wed, 10 Aug 2022 16:12:16 -0400 Subject: [PATCH 105/532] Enable localisation for `SettingSourceAttribute` --- .../Configuration/SettingSourceAttribute.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 21e67363c3..630b65ae82 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Reflection; using JetBrains.Annotations; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -43,12 +44,47 @@ namespace osu.Game.Configuration /// public Type? SettingControlType { get; set; } + public SettingSourceAttribute(Type declaringType, string label, string? description = null) + { + Label = getLocalisableStringFromMember(label) ?? string.Empty; + Description = getLocalisableStringFromMember(description) ?? string.Empty; + + LocalisableString? getLocalisableStringFromMember(string? member) + { + if (member == null) + return null; + + var property = declaringType.GetMember(member, BindingFlags.Static | BindingFlags.Public).FirstOrDefault(); + + if (property == null) + return null; + + switch (property) + { + case FieldInfo f: + return (LocalisableString)f.GetValue(null).AsNonNull(); + + case PropertyInfo p: + return (LocalisableString)p.GetValue(null).AsNonNull(); + + default: + throw new InvalidOperationException($"Member \"{member}\" was not found in type {declaringType} (must be a static field or property)"); + } + } + } + public SettingSourceAttribute(string? label, string? description = null) { Label = label ?? string.Empty; Description = description ?? string.Empty; } + public SettingSourceAttribute(Type declaringType, string label, string description, int orderPosition) + : this(declaringType, label, description) + { + OrderPosition = orderPosition; + } + public SettingSourceAttribute(string label, string description, int orderPosition) : this(label, description) { From 60abe8339801b2ddfd70d282f3d6e9dd02e403ee Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 10 Aug 2022 17:56:36 -0400 Subject: [PATCH 106/532] Remove newline --- osu.Game/OsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ca0ce916a2..073a99e406 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1144,7 +1144,6 @@ namespace osu.Game userProfile.Hide(); return true; } - else { ShowUser(new APIUser { Id = API.LocalUser.Value.Id }); From e01383b138f33407a4efc04c6fbcb8a7d6082c91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Aug 2022 13:17:14 +0900 Subject: [PATCH 107/532] Tidy up user passing logic --- osu.Game/OsuGame.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 073a99e406..f7747c5d64 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1140,15 +1140,10 @@ namespace osu.Game case GlobalAction.ToggleProfile: if (userProfile.State.Value == Visibility.Visible) - { userProfile.Hide(); - return true; - } else - { - ShowUser(new APIUser { Id = API.LocalUser.Value.Id }); - return true; - } + ShowUser(API.LocalUser.Value); + return true; case GlobalAction.RandomSkin: // Don't allow random skin selection while in the skin editor. From 3525dfb0f15242880092fde6ddc361b7e40b6c1d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 12 Aug 2022 01:17:33 +0200 Subject: [PATCH 108/532] added merging feature --- .../Edit/OsuSelectionHandler.cs | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 4a6d94f303..f9d4fbfc72 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -6,13 +6,16 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Utils; using osu.Game.Extensions; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -23,6 +26,12 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved(CanBeNull = true)] private IDistanceSnapProvider? snapProvider { get; set; } + [Resolved(CanBeNull = true)] + private EditorBeatmap? editorBeatmap { get; set; } + + [Resolved(CanBeNull = true)] + private IEditorChangeHandler? changeHandler { get; set; } + /// /// During a transform, the initial origin is stored so it can be used throughout the operation. /// @@ -322,5 +331,105 @@ namespace osu.Game.Rulesets.Osu.Edit private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType() .Where(h => !(h is Spinner)) .ToArray(); + + /// + /// All osu! hitobjects which can be merged. + /// + private OsuHitObject[] selectedMergeableObjects => SelectedItems.OfType() + .Where(h => h is HitCircle or Slider) + .OrderBy(h => h.StartTime) + .ToArray(); + + private void mergeSelection() + { + if (editorBeatmap == null || changeHandler == null || selectedMergeableObjects.Length < 2) + return; + + changeHandler.BeginChange(); + + // Have an initial slider object. + var firstHitObject = selectedMergeableObjects[0]; + var mergedHitObject = firstHitObject as Slider ?? new Slider + { + StartTime = firstHitObject.StartTime, + Position = firstHitObject.Position, + NewCombo = firstHitObject.NewCombo, + SampleControlPoint = firstHitObject.SampleControlPoint, + }; + + if (mergedHitObject.Path.ControlPoints.Count == 0) + { + mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.Linear)); + } + + // Merge all the selected hit objects into one slider path. + bool lastCircle = firstHitObject is HitCircle; + + foreach (var selectedMergeableObject in selectedMergeableObjects.Skip(1)) + { + if (selectedMergeableObject is IHasPath hasPath) + { + var offset = lastCircle ? selectedMergeableObject.Position - mergedHitObject.Position : mergedHitObject.Path.ControlPoints[^1].Position; + float distanceToLastControlPoint = Vector2.Distance(mergedHitObject.Path.ControlPoints[^1].Position, offset); + + // Calculate the distance required to travel to the expected distance of the merging slider. + mergedHitObject.Path.ExpectedDistance.Value = mergedHitObject.Path.CalculatedDistance + distanceToLastControlPoint + hasPath.Path.Distance; + + // Remove the last control point if it sits exactly on the start of the next control point. + if (Precision.AlmostEquals(distanceToLastControlPoint, 0)) + { + mergedHitObject.Path.ControlPoints.RemoveAt(mergedHitObject.Path.ControlPoints.Count - 1); + } + + mergedHitObject.Path.ControlPoints.AddRange(hasPath.Path.ControlPoints.Select(o => new PathControlPoint(o.Position + offset, o.Type))); + lastCircle = false; + } + else + { + // Turn the last control point into a linear type if this is the first merging circle in a sequence, so the subsequent control points can be inherited path type. + if (!lastCircle) + { + mergedHitObject.Path.ControlPoints.Last().Type = PathType.Linear; + } + + mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position)); + mergedHitObject.Path.ExpectedDistance.Value = null; + lastCircle = true; + } + } + + // Make sure only the merged hit object is in the beatmap. + if (firstHitObject is Slider) + { + foreach (var selectedMergeableObject in selectedMergeableObjects.Skip(1)) + { + editorBeatmap.Remove(selectedMergeableObject); + } + } + else + { + foreach (var selectedMergeableObject in selectedMergeableObjects) + { + editorBeatmap.Remove(selectedMergeableObject); + } + + editorBeatmap.Add(mergedHitObject); + } + + // Make sure the merged hitobject is selected. + SelectedItems.Clear(); + SelectedItems.Add(mergedHitObject); + + changeHandler.EndChange(); + } + + protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) + { + foreach (var item in base.GetContextMenuItemsForSelection(selection)) + yield return item; + + if (selection.Count() > 1 && selection.All(o => o.Item is HitCircle or Slider)) + yield return new OsuMenuItem("Merge selection", MenuItemType.Destructive, mergeSelection); + } } } From 037f56077bdbcc993e46f23fd682e43fc2f86941 Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 12 Aug 2022 13:29:04 +1000 Subject: [PATCH 109/532] Apply Flashlight grid nerf --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 12 ++++++++++++ .../Difficulty/Skills/Flashlight.cs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index fcf4179a3b..f1ae68ec73 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -18,11 +18,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const double min_velocity = 0.5; private const double slider_multiplier = 1.3; + private const double min_grid_multiplier = 0.35; + /// /// Evaluates the difficulty of memorising and hitting an object, based on: /// /// distance between a number of previous objects and the current object, /// the visual opacity of the current object, + /// the angle made by the current object, /// length and speed of the current object (for sliders), /// and whether the hidden mod is enabled. /// @@ -77,6 +80,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (hidden) result *= 1.0 + hidden_bonus; + // Nerf patterns with angles that are commonly used in grid maps. + // 0 deg, 60 deg, 120 deg and 180 deg are commonly used in hexgrid maps. + // 0 deg, 45 deg, 90 deg, 135 deg and 180 deg are commonly used in squaregrid maps. + if (osuCurrent.Angle != null) { + double hexgrid_multiplier = 1.0 - Math.Pow(Math.Cos((180 / 60.0) * (double)(osuCurrent.Angle)), 20.0); + double squaregrid_multiplier = 1.0 - Math.Pow(Math.Cos((180 / 45.0) * (double)(osuCurrent.Angle)), 20.0); + result *= (1.0 - min_grid_multiplier) * hexgrid_multiplier * squaregrid_multiplier + min_grid_multiplier; + } + double sliderBonus = 0.0; if (osuCurrent.BaseObject is Slider osuSlider) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 84ef109598..03130031ea 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills hasHiddenMod = mods.Any(m => m is OsuModHidden); } - private double skillMultiplier => 0.05; + private double skillMultiplier => 0.06; private double strainDecayBase => 0.15; private double currentStrain; From ac4213ecee5c2d2e22c8b7b0d0fbec1cc295443d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Aug 2022 12:30:11 +0900 Subject: [PATCH 110/532] Adjust relax mod multiplayer to 0.5x Has previously been discussed internally. Probably good to get this out before the next full reprocess of scores server-side. The multiplier here was @smoogipoo's suggested value. I'd be willing to go lower if this is seen at too high, but it should be a round number to make it easy for users to understand the max score available to them. --- osu.Game/Rulesets/Mods/ModRelax.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModRelax.cs b/osu.Game/Rulesets/Mods/ModRelax.cs index e5995ff180..a79c69b416 100644 --- a/osu.Game/Rulesets/Mods/ModRelax.cs +++ b/osu.Game/Rulesets/Mods/ModRelax.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "RX"; public override IconUsage? Icon => OsuIcon.ModRelax; public override ModType Type => ModType.Automation; - public override double ScoreMultiplier => 1; + public override double ScoreMultiplier => 0.5; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModFailCondition) }; } } From f70588a423b50c20030ab621ad6dd2d549ad935b Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 12 Aug 2022 14:08:32 +1000 Subject: [PATCH 111/532] Add newline before brace --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index f1ae68ec73..29434dcf49 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -83,7 +83,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Nerf patterns with angles that are commonly used in grid maps. // 0 deg, 60 deg, 120 deg and 180 deg are commonly used in hexgrid maps. // 0 deg, 45 deg, 90 deg, 135 deg and 180 deg are commonly used in squaregrid maps. - if (osuCurrent.Angle != null) { + if (osuCurrent.Angle != null) + { double hexgrid_multiplier = 1.0 - Math.Pow(Math.Cos((180 / 60.0) * (double)(osuCurrent.Angle)), 20.0); double squaregrid_multiplier = 1.0 - Math.Pow(Math.Cos((180 / 45.0) * (double)(osuCurrent.Angle)), 20.0); result *= (1.0 - min_grid_multiplier) * hexgrid_multiplier * squaregrid_multiplier + min_grid_multiplier; From 21c5fed45f48830d24ec448840b22f4b38e7504e Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 12 Aug 2022 14:09:16 +1000 Subject: [PATCH 112/532] Adjust capitalisation --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 29434dcf49..9d2696c978 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -85,9 +85,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // 0 deg, 45 deg, 90 deg, 135 deg and 180 deg are commonly used in squaregrid maps. if (osuCurrent.Angle != null) { - double hexgrid_multiplier = 1.0 - Math.Pow(Math.Cos((180 / 60.0) * (double)(osuCurrent.Angle)), 20.0); - double squaregrid_multiplier = 1.0 - Math.Pow(Math.Cos((180 / 45.0) * (double)(osuCurrent.Angle)), 20.0); - result *= (1.0 - min_grid_multiplier) * hexgrid_multiplier * squaregrid_multiplier + min_grid_multiplier; + double hexgridMultiplier = 1.0 - Math.Pow(Math.Cos((180 / 60.0) * (double)(osuCurrent.Angle)), 20.0); + double squaregridMultiplier = 1.0 - Math.Pow(Math.Cos((180 / 45.0) * (double)(osuCurrent.Angle)), 20.0); + result *= (1.0 - min_grid_multiplier) * hexgridMultiplier * squaregridMultiplier + min_grid_multiplier; } double sliderBonus = 0.0; From 38afc53bad96157459e66ee0920b8120e6bf375d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Aug 2022 13:40:29 +0900 Subject: [PATCH 113/532] Update interactive visual test runs to use development directory --- osu.Game/Tests/VisualTestRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/VisualTestRunner.cs b/osu.Game/Tests/VisualTestRunner.cs index bd98482768..c8279b9e3c 100644 --- a/osu.Game/Tests/VisualTestRunner.cs +++ b/osu.Game/Tests/VisualTestRunner.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true, })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development", new HostOptions { BindIPC = true, })) { host.Run(new OsuTestBrowser()); return 0; From 5111bad86ce647bc34c2873b7d9407a73323a477 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Aug 2022 14:15:12 +0900 Subject: [PATCH 114/532] Refactor `TestScenePlaylistOverlay` to use realm for testing Removes the dual-purpose flow which existed only for testing. --- .../UserInterface/TestScenePlaylistOverlay.cs | 48 ++++++++++++------- osu.Game/Overlays/Music/PlaylistOverlay.cs | 6 +-- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index ee5ef2f364..652afa80be 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -1,18 +1,20 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Bindables; +using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Database; -using osu.Game.Graphics.Containers; using osu.Game.Overlays.Music; +using osu.Game.Rulesets; using osu.Game.Tests.Resources; using osuTK; using osuTK.Input; @@ -21,13 +23,27 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestScenePlaylistOverlay : OsuManualInputManagerTestScene { - private readonly BindableList> beatmapSets = new BindableList>(); + protected override bool UseFreshStoragePerRun => true; - private PlaylistOverlay playlistOverlay; + private PlaylistOverlay playlistOverlay = null!; - private Live first; + private Live first = null!; - private const int item_count = 100; + private BeatmapManager beatmapManager = null!; + + private const int item_count = 20; + + private List beatmapSets => beatmapManager.GetAllUsableBeatmapSets(); + + [BackgroundDependencyLoader] + private void load(GameHost host) + { + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); + + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + } [SetUp] public void Setup() => Schedule(() => @@ -46,16 +62,12 @@ namespace osu.Game.Tests.Visual.UserInterface } }; - beatmapSets.Clear(); - for (int i = 0; i < item_count; i++) { - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo().ToLiveUnmanaged()); + beatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()); } - first = beatmapSets.First(); - - playlistOverlay.BeatmapSets.BindTo(beatmapSets); + first = beatmapSets.First().ToLive(Realm); }); [Test] @@ -70,9 +82,13 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("wait for animations to complete", () => !playlistOverlay.Transforms.Any()); + PlaylistItem firstItem = null!; + AddStep("hold 1st item handle", () => { - var handle = this.ChildrenOfType>.PlaylistItemHandle>().First(); + firstItem = this.ChildrenOfType().First(); + var handle = firstItem.ChildrenOfType().First(); + InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre); InputManager.PressButton(MouseButton.Left); }); @@ -83,7 +99,7 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.BottomLeft); }); - AddAssert("song 1 is 5th", () => beatmapSets[4].Equals(first)); + AddAssert("first is moved", () => playlistOverlay.ChildrenOfType().Single().Items.ElementAt(4).Value.Equals(firstItem.Model.Value)); AddStep("release handle", () => InputManager.ReleaseButton(MouseButton.Left)); } diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index e33fc8064f..9fe2fd5279 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -26,8 +26,6 @@ namespace osu.Game.Overlays.Music private const float transition_duration = 600; private const float playlist_height = 510; - public IBindableList> BeatmapSets => beatmapSets; - private readonly BindableList> beatmapSets = new BindableList>(); private readonly Bindable beatmap = new Bindable(); @@ -104,9 +102,7 @@ namespace osu.Game.Overlays.Music { base.LoadComplete(); - // tests might bind externally, in which case we don't want to involve realm. - if (beatmapSets.Count == 0) - beatmapSubscription = realm.RegisterForNotifications(r => r.All().Where(s => !s.DeletePending), beatmapsChanged); + beatmapSubscription = realm.RegisterForNotifications(r => r.All().Where(s => !s.DeletePending), beatmapsChanged); list.Items.BindTo(beatmapSets); beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true); From a90967715cdaa1b8b9830f37ef3c8681999cf64b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Aug 2022 14:44:19 +0900 Subject: [PATCH 115/532] Add test coverage of new imports not correctly being filtered by collection filter --- osu.Game.Tests/Resources/TestResources.cs | 5 +- .../UserInterface/TestScenePlaylistOverlay.cs | 66 ++++++++++++++++++- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index ee29cc8644..6bce03869d 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -128,6 +128,8 @@ namespace osu.Game.Tests.Resources var rulesetInfo = getRuleset(); + string hash = Guid.NewGuid().ToString().ComputeMD5Hash(); + yield return new BeatmapInfo { OnlineID = beatmapId, @@ -136,7 +138,8 @@ namespace osu.Game.Tests.Resources Length = length, BeatmapSet = beatmapSet, BPM = bpm, - Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + Hash = hash, + MD5Hash = hash, Ruleset = rulesetInfo, Metadata = metadata.DeepClone(), Difficulty = new BeatmapDifficulty diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index 652afa80be..e67eebf721 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -27,8 +27,6 @@ namespace osu.Game.Tests.Visual.UserInterface private PlaylistOverlay playlistOverlay = null!; - private Live first = null!; - private BeatmapManager beatmapManager = null!; private const int item_count = 20; @@ -67,7 +65,7 @@ namespace osu.Game.Tests.Visual.UserInterface beatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()); } - first = beatmapSets.First().ToLive(Realm); + beatmapSets.First().ToLive(Realm); }); [Test] @@ -117,6 +115,68 @@ namespace osu.Game.Tests.Visual.UserInterface () => playlistOverlay.ChildrenOfType() .Where(item => item.MatchingFilter) .All(item => item.FilterTerms.Any(term => term.ToString().Contains("10")))); + + AddStep("Import new non-matching beatmap", () => + { + var testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(1); + testBeatmapSetInfo.Beatmaps.Single().Metadata.Title = "no guid"; + beatmapManager.Import(testBeatmapSetInfo); + }); + + AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh())); + + AddAssert("results filtered correctly", + () => playlistOverlay.ChildrenOfType() + .Where(item => item.MatchingFilter) + .All(item => item.FilterTerms.Any(term => term.ToString().Contains("10")))); + } + + [Test] + public void TestCollectionFiltering() + { + NowPlayingCollectionDropdown collectionDropdown() => playlistOverlay.ChildrenOfType().Single(); + + AddStep("Add collection", () => + { + Dependencies.Get().Write(r => + { + r.RemoveAll(); + r.Add(new BeatmapCollection("wang")); + }); + }); + + AddUntilStep("wait for dropdown to have new collection", () => collectionDropdown().Items.Count() == 2); + + AddStep("Filter to collection", () => + { + collectionDropdown().Current.Value = collectionDropdown().Items.Last(); + }); + + AddUntilStep("No items present", () => !playlistOverlay.ChildrenOfType().Any(i => i.MatchingFilter)); + + AddStep("Import new non-matching beatmap", () => + { + beatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(1)); + }); + + AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh())); + + AddUntilStep("No items matching", () => !playlistOverlay.ChildrenOfType().Any(i => i.MatchingFilter)); + + BeatmapSetInfo collectionAddedBeatmapSet = null!; + + AddStep("Import new matching beatmap", () => + { + collectionAddedBeatmapSet = TestResources.CreateTestBeatmapSetInfo(1); + + beatmapManager.Import(collectionAddedBeatmapSet); + Realm.Write(r => r.All().First().BeatmapMD5Hashes.Add(collectionAddedBeatmapSet.Beatmaps.First().MD5Hash)); + }); + + AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh())); + + AddUntilStep("Only matching item", + () => playlistOverlay.ChildrenOfType().Where(i => i.MatchingFilter).Select(i => i.Model.ID), () => Is.EquivalentTo(new[] { collectionAddedBeatmapSet.ID })); } } } From b76e5757e17100c171df813235dc43d104e52c04 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Aug 2022 14:44:05 +0900 Subject: [PATCH 116/532] Fix `InSelectedCollection` not being applied to newly imported beatmaps --- osu.Game/Overlays/Music/Playlist.cs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/Music/Playlist.cs b/osu.Game/Overlays/Music/Playlist.cs index 2bb0ff1085..15fc54a337 100644 --- a/osu.Game/Overlays/Music/Playlist.cs +++ b/osu.Game/Overlays/Music/Playlist.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using osu.Framework.Bindables; @@ -17,10 +15,12 @@ namespace osu.Game.Overlays.Music { public class Playlist : OsuRearrangeableListContainer> { - public Action> RequestSelection; + public Action>? RequestSelection; public readonly Bindable> SelectedSet = new Bindable>(); + private FilterCriteria currentCriteria = new FilterCriteria(); + public new MarginPadding Padding { get => base.Padding; @@ -31,26 +31,22 @@ namespace osu.Game.Overlays.Music { var items = (SearchContainer>>)ListContainer; - string[] currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray()); + string[]? currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray()); foreach (var item in items.OfType()) { - if (currentCollectionHashes == null) - item.InSelectedCollection = true; - else - { - item.InSelectedCollection = item.Model.Value.Beatmaps.Select(b => b.MD5Hash) - .Any(currentCollectionHashes.Contains); - } + item.InSelectedCollection = currentCollectionHashes == null || item.Model.Value.Beatmaps.Select(b => b.MD5Hash).Any(currentCollectionHashes.Contains); } items.SearchTerm = criteria.SearchText; + currentCriteria = criteria; } - public Live FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); + public Live? FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); protected override OsuRearrangeableListItem> CreateOsuDrawable(Live item) => new PlaylistItem(item) { + InSelectedCollection = currentCriteria.Collection?.PerformRead(c => item.Value.Beatmaps.Select(b => b.MD5Hash).Any(c.BeatmapMD5Hashes.Contains)) != false, SelectedSet = { BindTarget = SelectedSet }, RequestSelection = set => RequestSelection?.Invoke(set) }; From e5e9841652c84ac5ab688d6c1137f83349b56a85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Aug 2022 15:25:35 +0900 Subject: [PATCH 117/532] Apply multiple other mod debuffs as decided in pull request discussion --- osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs | 2 +- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 2 +- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 2 +- osu.Game/Rulesets/Mods/ModRelax.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs index 614ef76a3b..73dfaaa878 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Acronym => "CS"; - public override double ScoreMultiplier => 1; + public override double ScoreMultiplier => 0.9; public override string Description => "No more tricky speed changes!"; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 872fcf7e9b..9229c0393d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override IconUsage? Icon => OsuIcon.ModAutopilot; public override ModType Type => ModType.Automation; public override string Description => @"Automatic cursor movement - just follow the rhythm."; - public override double ScoreMultiplier => 1; + public override double ScoreMultiplier => 0.1; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) }; public bool PerformFail() => false; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index 9316f9ed74..7f7d6f70d2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override IconUsage? Icon => FontAwesome.Solid.Magnet; public override ModType Type => ModType.Fun; public override string Description => "No need to chase the circles – your cursor is a magnet!"; - public override double ScoreMultiplier => 1; + public override double ScoreMultiplier => 0.5; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) }; private IFrameStableClock gameplayClock = null!; diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 38d26ed05a..7b84db844b 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.Fun; - public override double ScoreMultiplier => 1; + public override double ScoreMultiplier => 0.5; public override bool ValidForMultiplayer => false; public override bool ValidForMultiplayerAsFreeMod => false; diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index eefa1531c4..62a257608a 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => FontAwesome.Solid.Hammer; - public override double ScoreMultiplier => 1.0; + public sealed override double ScoreMultiplier => 0.5; public override bool RequiresConfiguration => true; diff --git a/osu.Game/Rulesets/Mods/ModRelax.cs b/osu.Game/Rulesets/Mods/ModRelax.cs index a79c69b416..e1506e3a12 100644 --- a/osu.Game/Rulesets/Mods/ModRelax.cs +++ b/osu.Game/Rulesets/Mods/ModRelax.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "RX"; public override IconUsage? Icon => OsuIcon.ModRelax; public override ModType Type => ModType.Automation; - public override double ScoreMultiplier => 0.5; + public sealed override double ScoreMultiplier => 0.1; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModFailCondition) }; } } From 9d1b0b5836395db80779f25e3b184904857641d0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 12 Aug 2022 22:32:27 +0900 Subject: [PATCH 118/532] Revert sealing --- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 2 +- osu.Game/Rulesets/Mods/ModRelax.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 62a257608a..b7435ec3ec 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => FontAwesome.Solid.Hammer; - public sealed override double ScoreMultiplier => 0.5; + public override double ScoreMultiplier => 0.5; public override bool RequiresConfiguration => true; diff --git a/osu.Game/Rulesets/Mods/ModRelax.cs b/osu.Game/Rulesets/Mods/ModRelax.cs index e1506e3a12..49c10339ee 100644 --- a/osu.Game/Rulesets/Mods/ModRelax.cs +++ b/osu.Game/Rulesets/Mods/ModRelax.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "RX"; public override IconUsage? Icon => OsuIcon.ModRelax; public override ModType Type => ModType.Automation; - public sealed override double ScoreMultiplier => 0.1; + public override double ScoreMultiplier => 0.1; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModFailCondition) }; } } From b05acb00738ed25463fbdacbee9b7da68ed01f58 Mon Sep 17 00:00:00 2001 From: basseX Date: Sat, 13 Aug 2022 21:32:24 +0200 Subject: [PATCH 119/532] Make `CommentMarkdownTextFlowContainer` render images --- osu.Game/Overlays/Comments/CommentMarkdownContainer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs index e94a1b0147..58f020fd9e 100644 --- a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs +++ b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs @@ -18,8 +18,10 @@ namespace osu.Game.Overlays.Comments private class CommentMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer { - // Don't render image in comment for now - protected override void AddImage(LinkInline linkInline) { } + protected override void AddImage(LinkInline linkInline) + { + AddDrawable(new OsuMarkdownImage(linkInline)); + } } private class CommentMarkdownHeading : OsuMarkdownHeading From 932becc4b227ff2eef4419be68a16fae5151888e Mon Sep 17 00:00:00 2001 From: basseX Date: Sun, 14 Aug 2022 10:11:49 +0200 Subject: [PATCH 120/532] Remove `CommentMarkdownTextFlowContainer` and rather use base-class `OsuMarkdownTextFlowContainer` --- .../Overlays/Comments/CommentMarkdownContainer.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs index 58f020fd9e..45b8172994 100644 --- a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs +++ b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs @@ -4,7 +4,6 @@ #nullable disable using Markdig.Syntax; -using Markdig.Syntax.Inlines; using osu.Framework.Graphics.Containers.Markdown; using osu.Game.Graphics.Containers.Markdown; @@ -12,18 +11,10 @@ namespace osu.Game.Overlays.Comments { public class CommentMarkdownContainer : OsuMarkdownContainer { - public override MarkdownTextFlowContainer CreateTextFlow() => new CommentMarkdownTextFlowContainer(); + public override MarkdownTextFlowContainer CreateTextFlow() => new OsuMarkdownTextFlowContainer(); protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock); - private class CommentMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer - { - protected override void AddImage(LinkInline linkInline) - { - AddDrawable(new OsuMarkdownImage(linkInline)); - } - } - private class CommentMarkdownHeading : OsuMarkdownHeading { public CommentMarkdownHeading(HeadingBlock headingBlock) From 383afe04f35159262e34d437c7835e292d81125a Mon Sep 17 00:00:00 2001 From: basseX Date: Sun, 14 Aug 2022 15:15:36 +0200 Subject: [PATCH 121/532] Remove not needed override --- osu.Game/Overlays/Comments/CommentMarkdownContainer.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs index 45b8172994..8fc011b2bf 100644 --- a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs +++ b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs @@ -11,8 +11,6 @@ namespace osu.Game.Overlays.Comments { public class CommentMarkdownContainer : OsuMarkdownContainer { - public override MarkdownTextFlowContainer CreateTextFlow() => new OsuMarkdownTextFlowContainer(); - protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock); private class CommentMarkdownHeading : OsuMarkdownHeading From 18ce784ae0270137909b820836e825d6d4ab9319 Mon Sep 17 00:00:00 2001 From: naoei Date: Sun, 14 Aug 2022 14:51:35 -0400 Subject: [PATCH 122/532] Allow StatisticItem's name param to be nullable --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- osu.Game/Screens/Ranking/Statistics/StatisticItem.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 4723416c30..f787bb3c41 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -395,7 +395,7 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem(null, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new AverageHitError(score.HitEvents), new UnstableRate(score.HitEvents) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 302194e91a..8070e6464d 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -316,7 +316,7 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem(null, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 223e268d7f..961864c36a 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem(null, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index baabc90c6f..a3c51b4876 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// A function returning the content to be displayed. /// Whether this item requires hit events. If true, will not be called if no hit events are available. /// The of this item. This can be thought of as the column dimension of an encompassing . - public StatisticItem(LocalisableString name, [NotNull] Func createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null) + public StatisticItem(LocalisableString? name, [NotNull] Func createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null) { Name = name; RequiresHitEvents = requiresHitEvents; From 45e9eda9e7cb98b7c2e57019e692f0bf2c0b940d Mon Sep 17 00:00:00 2001 From: naoei Date: Sun, 14 Aug 2022 14:54:02 -0400 Subject: [PATCH 123/532] Localise hit result name --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 3 ++- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 3 ++- osu.Game.Rulesets.Osu/OsuRuleset.cs | 3 ++- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 3 ++- osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs | 8 +++++--- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 4 ++-- osu.Game/Rulesets/Ruleset.cs | 5 +++-- osu.Game/Scoring/HitResultDisplayStatistic.cs | 5 +++-- 8 files changed, 21 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index f832d99807..ed151855b1 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -23,6 +23,7 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Scoring; using System; using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Localisation; using osu.Game.Rulesets.Catch.Edit; using osu.Game.Rulesets.Catch.Skinning.Legacy; using osu.Game.Rulesets.Edit; @@ -162,7 +163,7 @@ namespace osu.Game.Rulesets.Catch }; } - public override string GetDisplayNameForHitResult(HitResult result) + public override LocalisableString GetDisplayNameForHitResult(HitResult result) { switch (result) { diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index f787bb3c41..2167e5e5ac 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -15,6 +15,7 @@ using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Replays.Types; @@ -356,7 +357,7 @@ namespace osu.Game.Rulesets.Mania }; } - public override string GetDisplayNameForHitResult(HitResult result) + public override LocalisableString GetDisplayNameForHitResult(HitResult result) { switch (result) { diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 8070e6464d..f7df949414 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -32,6 +32,7 @@ using osu.Game.Skinning; using System; using System.Linq; using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Localisation; using osu.Game.Rulesets.Osu.Edit.Setup; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Skinning.Legacy; @@ -253,7 +254,7 @@ namespace osu.Game.Rulesets.Osu }; } - public override string GetDisplayNameForHitResult(HitResult result) + public override LocalisableString GetDisplayNameForHitResult(HitResult result) { switch (result) { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 961864c36a..555c272954 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -25,6 +25,7 @@ using osu.Game.Scoring; using System; using System.Linq; using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Localisation; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Edit; using osu.Game.Rulesets.Taiko.Objects; @@ -192,7 +193,7 @@ namespace osu.Game.Rulesets.Taiko }; } - public override string GetDisplayNameForHitResult(HitResult result) + public override LocalisableString GetDisplayNameForHitResult(HitResult result) { switch (result) { diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index 53a4719050..2f3ece0e3b 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs @@ -10,6 +10,8 @@ using osuTK; using osu.Game.Graphics.Sprites; using osu.Game.Graphics; using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Localisation; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -126,7 +128,7 @@ namespace osu.Game.Online.Leaderboards private class HitResultCell : CompositeDrawable { - private readonly string displayName; + private readonly LocalisableString displayName; private readonly HitResult result; private readonly int count; @@ -134,7 +136,7 @@ namespace osu.Game.Online.Leaderboards { AutoSizeAxes = Axes.Both; - displayName = stat.DisplayName; + displayName = stat.DisplayName.ToUpper(); result = stat.Result; count = stat.Count; } @@ -153,7 +155,7 @@ namespace osu.Game.Online.Leaderboards new OsuSpriteText { Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), - Text = displayName.ToUpperInvariant(), + Text = displayName.ToUpper(), Colour = colours.ForHitResult(result), }, new OsuSpriteText diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 5463c7a50f..11aefd435d 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -59,7 +59,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores /// /// The statistics that appear in the table, in order of appearance. /// - private readonly List<(HitResult result, string displayName)> statisticResultTypes = new List<(HitResult, string)>(); + private readonly List<(HitResult result, LocalisableString displayName)> statisticResultTypes = new List<(HitResult, LocalisableString)>(); private bool showPerformancePoints; @@ -114,7 +114,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores if (result.IsBonus()) continue; - string displayName = ruleset.GetDisplayNameForHitResult(result); + var displayName = ruleset.GetDisplayNameForHitResult(result); columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60))); statisticResultTypes.Add((result, displayName)); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index c1ec6c30ef..50ce6b3b12 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -28,6 +28,7 @@ using osu.Game.Users; using JetBrains.Annotations; using osu.Framework.Extensions; using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Extensions; using osu.Game.Rulesets.Filter; @@ -313,7 +314,7 @@ namespace osu.Game.Rulesets /// /// All valid s along with a display-friendly name. /// - public IEnumerable<(HitResult result, string displayName)> GetHitResults() + public IEnumerable<(HitResult result, LocalisableString displayName)> GetHitResults() { var validResults = GetValidHitResults(); @@ -351,7 +352,7 @@ namespace osu.Game.Rulesets /// /// The result type to get the name for. /// The display name. - public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription(); + public virtual LocalisableString GetDisplayNameForHitResult(HitResult result) => result.GetLocalisableDescription(); /// /// Creates ruleset-specific beatmap filter criteria to be used on the song select screen. diff --git a/osu.Game/Scoring/HitResultDisplayStatistic.cs b/osu.Game/Scoring/HitResultDisplayStatistic.cs index 4603ff053e..20deff4875 100644 --- a/osu.Game/Scoring/HitResultDisplayStatistic.cs +++ b/osu.Game/Scoring/HitResultDisplayStatistic.cs @@ -3,6 +3,7 @@ #nullable disable +using osu.Framework.Localisation; using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring @@ -30,9 +31,9 @@ namespace osu.Game.Scoring /// /// A custom display name for the result type. May be provided by rulesets to give better clarity. /// - public string DisplayName { get; } + public LocalisableString DisplayName { get; } - public HitResultDisplayStatistic(HitResult result, int count, int? maxCount, string displayName) + public HitResultDisplayStatistic(HitResult result, int count, int? maxCount, LocalisableString displayName) { Result = result; Count = count; From 784ce4d23ded91bd0a13c36aabe432bb27445670 Mon Sep 17 00:00:00 2001 From: naoei Date: Sun, 14 Aug 2022 15:03:05 -0400 Subject: [PATCH 124/532] Add test coverage for localisable setting source --- .../Visual/Settings/TestSceneSettingsSource.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs index 98de712703..dc2a687bd5 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Overlays.Settings; using osuTK; @@ -39,6 +40,9 @@ namespace osu.Game.Tests.Visual.Settings [SettingSource("Sample bool", "Clicking this changes a setting")] public BindableBool TickBindable { get; } = new BindableBool(); + [SettingSource(typeof(TestStrings), nameof(TestStrings.LocalisableLabel), nameof(TestStrings.LocalisableDescription))] + public BindableBool LocalisableBindable { get; } = new BindableBool(true); + [SettingSource("Sample float", "Change something for a mod")] public BindableFloat SliderBindable { get; } = new BindableFloat { @@ -75,5 +79,11 @@ namespace osu.Game.Tests.Visual.Settings Value1, Value2 } + + private class TestStrings + { + public static LocalisableString LocalisableLabel => new LocalisableString("Sample localisable label"); + public static LocalisableString LocalisableDescription => new LocalisableString("Sample localisable description"); + } } } From 623e90a7b23da45c21d4449b7efb058f6113e808 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 15:05:35 +0900 Subject: [PATCH 125/532] Fix div-by-zero in `SongProgress` when no object duration could be calculated --- osu.Game/Screens/Play/HUD/SongProgress.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/SongProgress.cs b/osu.Game/Screens/Play/HUD/SongProgress.cs index 09afd7a9d3..85bb42193b 100644 --- a/osu.Game/Screens/Play/HUD/SongProgress.cs +++ b/osu.Game/Screens/Play/HUD/SongProgress.cs @@ -94,7 +94,10 @@ namespace osu.Game.Screens.Play.HUD double objectOffsetCurrent = currentTime - FirstHitTime; double objectDuration = LastHitTime - FirstHitTime; - UpdateProgress(objectOffsetCurrent / objectDuration, false); + if (objectDuration == 0) + UpdateProgress(0, false); + else + UpdateProgress(objectOffsetCurrent / objectDuration, false); } } } From 00879357083fca475c4cfabc4a9acc67b7765b8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 18:05:19 +0900 Subject: [PATCH 126/532] Update `TestSceneSpinnerRotation` to use constraint-based assertions --- .../TestSceneSpinnerRotation.cs | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index d1796f2231..b7f91c22f4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -12,7 +10,6 @@ using osu.Framework.Audio; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Timing; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Replays; using osu.Game.Rulesets.Objects; @@ -36,16 +33,16 @@ namespace osu.Game.Rulesets.Osu.Tests private const double spinner_duration = 6000; [Resolved] - private AudioManager audioManager { get; set; } + private AudioManager audioManager { get; set; } = null!; protected override bool Autoplay => true; protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer(); - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); - private DrawableSpinner drawableSpinner; + private DrawableSpinner drawableSpinner = null!; private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType().Single(); [SetUpSteps] @@ -67,12 +64,12 @@ namespace osu.Game.Rulesets.Osu.Tests { trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); }); - AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100)); - AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100)); + AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100)); + AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.Not.EqualTo(0).Within(100)); addSeekStep(0); - AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance)); - AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100)); + AddAssert("is disc rotation almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(0).Within(trackerRotationTolerance)); + AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(0).Within(100)); } [Test] @@ -100,20 +97,20 @@ namespace osu.Game.Rulesets.Osu.Tests // we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in. // due to the exponential damping applied we're allowing a larger margin of error of about 10% // (5% relative to the final rotation value, but we're half-way through the spin). - () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation / 2, trackerRotationTolerance)); + () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance)); AddAssert("symbol rotation rewound", - () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance)); + () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation / 2).Within(spinnerSymbolRotationTolerance)); AddAssert("is cumulative rotation rewound", // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. - () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100)); + () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100)); addSeekStep(spinner_start_time + 5000); AddAssert("is disc rotation almost same", - () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance)); + () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance)); AddAssert("is symbol rotation almost same", - () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance)); + () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation).Within(spinnerSymbolRotationTolerance)); AddAssert("is cumulative rotation almost same", - () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation, 100)); + () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100)); } [Test] @@ -177,10 +174,10 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value); addSeekStep(2000); - AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0)); + AddAssert("spm still valid", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(estimatedSpm).Within(1.0)); addSeekStep(1000); - AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0)); + AddAssert("spm still valid", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(estimatedSpm).Within(1.0)); } [TestCase(0.5)] @@ -202,14 +199,14 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate); addSeekStep(1000); - AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05)); - AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0)); + AddAssert("progress almost same", () => expectedProgress, () => Is.EqualTo(drawableSpinner.Progress).Within(0.05)); + AddAssert("spm almost same", () => expectedSpm, () => Is.EqualTo(drawableSpinner.SpinsPerMinute.Value).Within(2.0)); } private void addSeekStep(double time) { AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time)); - AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); + AddUntilStep("wait for seek to finish", () => time, () => Is.EqualTo(Player.DrawableRuleset.FrameStableClock.CurrentTime).Within(100)); } private void transformReplay(Func replayTransformation) => AddStep("set replay", () => From 58146598c8d4513011748a39a5ad97bd2a36f355 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 18:06:42 +0900 Subject: [PATCH 127/532] Update `TestSceneEditorClock` to use constraint-based assertions --- .../Visual/Editing/TestSceneEditorClock.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs index 96ba802a5f..3c6820e49b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -65,7 +63,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("seek near end", () => Clock.Seek(Clock.TrackLength - 250)); AddUntilStep("clock stops", () => !Clock.IsRunning); - AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength); + AddUntilStep("clock stopped at end", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); AddStep("start clock again", () => Clock.Start()); AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); @@ -80,7 +78,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("clock stopped", () => !Clock.IsRunning); AddStep("seek exactly to end", () => Clock.Seek(Clock.TrackLength)); - AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength); + AddAssert("clock stopped at end", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); AddStep("start clock again", () => Clock.Start()); AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); @@ -92,16 +90,16 @@ namespace osu.Game.Tests.Visual.Editing AddStep("stop clock", () => Clock.Stop()); AddStep("seek before start time", () => Clock.Seek(-1000)); - AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0); + AddAssert("time is clamped to 0", () => Clock.CurrentTime, () => Is.EqualTo(0)); AddStep("seek beyond track length", () => Clock.Seek(Clock.TrackLength + 1000)); - AddAssert("time is clamped to track length", () => Clock.CurrentTime == Clock.TrackLength); + AddAssert("time is clamped to track length", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); AddStep("seek smoothly before start time", () => Clock.SeekSmoothlyTo(-1000)); - AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0); + AddUntilStep("time is clamped to 0", () => Clock.CurrentTime, () => Is.EqualTo(0)); AddStep("seek smoothly beyond track length", () => Clock.SeekSmoothlyTo(Clock.TrackLength + 1000)); - AddAssert("time is clamped to track length", () => Clock.CurrentTime == Clock.TrackLength); + AddUntilStep("time is clamped to track length", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); } protected override void Dispose(bool isDisposing) From 95c1b488a7ebfcf4872b5249f9e266940c7d7fb4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 16:55:55 +0900 Subject: [PATCH 128/532] Add non-null assertion to `FrameStabilityContainer` --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index dcd7141419..b4a0d83bf2 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -129,6 +130,8 @@ namespace osu.Game.Rulesets.UI if (parentGameplayClock == null) setClock(); // LoadComplete may not be run yet, but we still want the clock. + Debug.Assert(parentGameplayClock != null); + double proposedTime = parentGameplayClock.CurrentTime; if (FrameStablePlayback) From 224f3eaa8470a0ea66e596122e0fb0fc78d6c788 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 16:56:16 +0900 Subject: [PATCH 129/532] Make `GameplayClockContainer` non-`abstract` and use in `MultiSpectatorPlayer` --- .../Spectate/MultiSpectatorPlayer.cs | 43 +++++++------------ .../Screens/Play/GameplayClockContainer.cs | 8 ++-- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index a0558f97a9..68eae76030 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -1,12 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -26,7 +22,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// The score containing the player's replay. /// The clock controlling the gameplay running state. - public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISpectatorPlayerClock spectatorPlayerClock) + public MultiSpectatorPlayer(Score score, ISpectatorPlayerClock spectatorPlayerClock) : base(score, new PlayerConfiguration { AllowUserInteraction = false }) { this.spectatorPlayerClock = spectatorPlayerClock; @@ -41,6 +37,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate HUDOverlay.HoldToQuit.Expire(); } + protected override void Update() + { + // The player clock's running state is controlled externally, but the local pausing state needs to be updated to start/stop gameplay. + CatchUpSpectatorPlayerClock catchUpClock = (CatchUpSpectatorPlayerClock)GameplayClockContainer.SourceClock; + + if (catchUpClock.IsRunning) + GameplayClockContainer.Start(); + else + GameplayClockContainer.Stop(); + + base.Update(); + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -50,28 +59,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) - => new SpectatorGameplayClockContainer(spectatorPlayerClock); - - private class SpectatorGameplayClockContainer : GameplayClockContainer - { - public SpectatorGameplayClockContainer([NotNull] IClock sourceClock) - : base(sourceClock) - { - } - - protected override void Update() - { - // The SourceClock here is always a CatchUpSpectatorPlayerClock. - // The player clock's running state is controlled externally, but the local pausing state needs to be updated to stop gameplay. - if (SourceClock.IsRunning) - Start(); - else - Stop(); - - base.Update(); - } - - protected override GameplayClock CreateGameplayClock(IFrameBasedClock source) => new GameplayClock(source); - } + => new GameplayClockContainer(spectatorPlayerClock); } } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index b37d15e06c..ffecb1d9a5 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Play /// /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use. /// - public abstract class GameplayClockContainer : Container, IAdjustableClock + public class GameplayClockContainer : Container, IAdjustableClock { /// /// The final clock which is exposed to gameplay components. @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Play /// /// The source clock. /// - protected IClock SourceClock { get; private set; } + public IClock SourceClock { get; private set; } /// /// Invoked when a seek has been performed via @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play /// Creates a new . /// /// The source used for timing. - protected GameplayClockContainer(IClock sourceClock) + public GameplayClockContainer(IClock sourceClock) { SourceClock = sourceClock; @@ -193,7 +193,7 @@ namespace osu.Game.Screens.Play /// /// The providing the source time. /// The final . - protected abstract GameplayClock CreateGameplayClock(IFrameBasedClock source); + protected virtual GameplayClock CreateGameplayClock(IFrameBasedClock source) => new GameplayClock(source); #region IAdjustableClock From 6d782181427564409bb9c4b030f1e49d87db4dd7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 17:06:24 +0900 Subject: [PATCH 130/532] Update usages of `GameplayClockContainer.GameplayClock` to access properties directly --- .../Mods/TestSceneOsuModDoubleTime.cs | 2 +- .../TestSceneMasterGameplayClockContainer.cs | 8 ++--- .../Gameplay/TestSceneStoryboardSamples.cs | 2 +- .../Visual/Gameplay/TestSceneLeadIn.cs | 4 +-- .../Gameplay/TestSceneOverlayActivation.cs | 2 +- .../Visual/Gameplay/TestScenePause.cs | 4 +-- .../Visual/Gameplay/TestSceneSkipOverlay.cs | 5 +-- .../Visual/Gameplay/TestSceneSongProgress.cs | 2 +- .../Gameplay/TestSceneStoryboardWithOutro.cs | 10 +++--- .../TestSceneMultiSpectatorScreen.cs | 2 +- .../Multiplayer/TestSceneMultiplayer.cs | 2 +- .../Spectate/MultiSpectatorScreen.cs | 2 +- .../Screens/Play/GameplayClockContainer.cs | 32 ++++++++++++------- osu.Game/Screens/Play/Player.cs | 8 ++--- 14 files changed, 48 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs index 335ef31019..8df8afe147 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { Mod = mod, PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 && - Precision.AlmostEquals(Player.GameplayClockContainer.GameplayClock.Rate, mod.SpeedChange.Value) + Precision.AlmostEquals(Player.GameplayClockContainer.Rate, mod.SpeedChange.Value) }); } } diff --git a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs index 5f403f9487..abd734b96c 100644 --- a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Gameplay }); AddStep("start clock", () => gameplayClockContainer.Start()); - AddUntilStep("elapsed greater than zero", () => gameplayClockContainer.GameplayClock.ElapsedFrameTime > 0); + AddUntilStep("elapsed greater than zero", () => gameplayClockContainer.ElapsedFrameTime > 0); } [Test] @@ -60,16 +60,16 @@ namespace osu.Game.Tests.Gameplay }); AddStep("start clock", () => gameplayClockContainer.Start()); - AddUntilStep("current time greater 2000", () => gameplayClockContainer.GameplayClock.CurrentTime > 2000); + AddUntilStep("current time greater 2000", () => gameplayClockContainer.CurrentTime > 2000); double timeAtReset = 0; AddStep("reset clock", () => { - timeAtReset = gameplayClockContainer.GameplayClock.CurrentTime; + timeAtReset = gameplayClockContainer.CurrentTime; gameplayClockContainer.Reset(); }); - AddAssert("current time < time at reset", () => gameplayClockContainer.GameplayClock.CurrentTime < timeAtReset); + AddAssert("current time < time at reset", () => gameplayClockContainer.CurrentTime < timeAtReset); } [Test] diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index f05244ab88..3ccf6c5d33 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Gameplay beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) { - Clock = gameplayContainer.GameplayClock + Clock = gameplayContainer }); }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index 7f4276f819..0d80d29cab 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Gameplay public double FirstHitObjectTime => DrawableRuleset.Objects.First().StartTime; - public double GameplayClockTime => GameplayClockContainer.GameplayClock.CurrentTime; + public double GameplayClockTime => GameplayClockContainer.CurrentTime; protected override void UpdateAfterChildren() { @@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Gameplay if (!FirstFrameClockTime.HasValue) { - FirstFrameClockTime = GameplayClockContainer.GameplayClock.CurrentTime; + FirstFrameClockTime = GameplayClockContainer.CurrentTime; AddInternal(new OsuSpriteText { Text = $"GameplayStartTime: {DrawableRuleset.GameplayStartTime} " diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index 3e637f1870..789e7e770f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Gameplay base.SetUpSteps(); AddUntilStep("gameplay has started", - () => Player.GameplayClockContainer.GameplayClock.CurrentTime > Player.DrawableRuleset.GameplayStartTime); + () => Player.GameplayClockContainer.CurrentTime > Player.DrawableRuleset.GameplayStartTime); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 71cc1f7b23..cad8c62233 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -313,7 +313,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("pause again", () => { Player.Pause(); - return !Player.GameplayClockContainer.GameplayClock.IsRunning; + return !Player.GameplayClockContainer.IsRunning; }); AddAssert("loop is playing", () => getLoop().IsPlaying); @@ -378,7 +378,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("pause overlay " + (isShown ? "shown" : "hidden"), () => Player.PauseOverlayVisible == isShown); private void confirmClockRunning(bool isRunning) => - AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.GameplayClock.IsRunning == isRunning); + AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.IsRunning == isRunning); protected override bool AllowFail => true; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 5c73db15df..b6b3650c83 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Timing; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Play; using osuTK; @@ -22,7 +23,7 @@ namespace osu.Game.Tests.Visual.Gameplay private double increment; private GameplayClockContainer gameplayClockContainer; - private GameplayClock gameplayClock; + private IFrameBasedClock gameplayClock; private const double skip_time = 6000; @@ -51,7 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; gameplayClockContainer.Start(); - gameplayClock = gameplayClockContainer.GameplayClock; + gameplayClock = gameplayClockContainer; }); [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs index 9eb71b9cf7..146cbfb052 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Gameplay Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)); - Dependencies.CacheAs(gameplayClockContainer.GameplayClock); + Dependencies.CacheAs(gameplayClockContainer); } [SetUpSteps] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index e2b2ad85a3..e2c825df0b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestStoryboardNoSkipOutro() { CreateTest(); - AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); + AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); AddUntilStep("wait for score shown", () => Player.IsScoreShown); } @@ -100,7 +100,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); - AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); + AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); } @@ -111,7 +111,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("set ShowResults = false", () => showResults = false); }); - AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); + AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); AddWaitStep("wait", 10); AddAssert("no score shown", () => !Player.IsScoreShown); } @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestStoryboardEndsBeforeCompletion() { CreateTest(() => AddStep("set storyboard duration to .1s", () => currentStoryboardDuration = 100)); - AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); + AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("wait for score shown", () => Player.IsScoreShown); } @@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("skip overlay content not visible", () => fadeContainer().State == Visibility.Hidden); AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible); - AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); + AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 706d493fd6..a2e3ab7318 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -451,7 +451,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } private void checkPaused(int userId, bool state) - => AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); + => AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().IsRunning != state); private void checkPausedInstant(int userId, bool state) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 269867be73..6098a3e794 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -671,7 +671,7 @@ namespace osu.Game.Tests.Visual.Multiplayer for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000) { double time = i; - AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType().SingleOrDefault()?.GameplayClock.CurrentTime > time); + AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType().SingleOrDefault()?.CurrentTime > time); } AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 5cd9e0ddf9..7ed0be50e5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -126,7 +126,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate for (int i = 0; i < Users.Count; i++) { - grid.Add(instances[i] = new PlayerArea(Users[i], masterClockContainer.GameplayClock)); + grid.Add(instances[i] = new PlayerArea(Users[i], masterClockContainer)); syncManager.AddPlayerClock(instances[i].GameplayClock); } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index ffecb1d9a5..2cc1fe8eaf 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -1,11 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -16,13 +15,8 @@ namespace osu.Game.Screens.Play /// /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use. /// - public class GameplayClockContainer : Container, IAdjustableClock + public class GameplayClockContainer : Container, IAdjustableClock, IFrameBasedClock { - /// - /// The final clock which is exposed to gameplay components. - /// - public GameplayClock GameplayClock { get; private set; } - /// /// Whether gameplay is paused. /// @@ -41,7 +35,7 @@ namespace osu.Game.Screens.Play /// /// Invoked when a seek has been performed via /// - public event Action OnSeek; + public event Action? OnSeek; private double? startTime; @@ -59,11 +53,16 @@ namespace osu.Game.Screens.Play { startTime = value; - if (GameplayClock != null) + if (GameplayClock.IsNotNull()) GameplayClock.StartTime = value; } } + /// + /// The final clock which is exposed to gameplay components. + /// + protected GameplayClock GameplayClock { get; private set; } = null!; + /// /// Creates a new . /// @@ -215,12 +214,23 @@ namespace osu.Game.Screens.Play set => throw new NotSupportedException(); } - double IClock.Rate => GameplayClock.Rate; + public double Rate => GameplayClock.Rate; public double CurrentTime => GameplayClock.CurrentTime; public bool IsRunning => GameplayClock.IsRunning; #endregion + + public void ProcessFrame() + { + // Handled via update. Don't process here to safeguard from external usages potentially processing frames additional times. + } + + public double ElapsedFrameTime => GameplayClock.ElapsedFrameTime; + + public double FramesPerSecond => GameplayClock.FramesPerSecond; + + public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo; } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 08b6da1921..0ef09e4029 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -475,7 +475,7 @@ namespace osu.Game.Screens.Play private void updateSampleDisabledState() { - samplePlaybackDisabled.Value = DrawableRuleset.FrameStableClock.IsCatchingUp.Value || GameplayClockContainer.GameplayClock.IsPaused.Value; + samplePlaybackDisabled.Value = DrawableRuleset.FrameStableClock.IsCatchingUp.Value || GameplayClockContainer.IsPaused.Value; } private void updatePauseOnFocusLostState() @@ -877,7 +877,7 @@ namespace osu.Game.Screens.Play private double? lastPauseActionTime; protected bool PauseCooldownActive => - lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown; + lastPauseActionTime.HasValue && GameplayClockContainer.CurrentTime < lastPauseActionTime + pause_cooldown; /// /// A set of conditionals which defines whether the current game state and configuration allows for @@ -915,7 +915,7 @@ namespace osu.Game.Screens.Play GameplayClockContainer.Stop(); PauseOverlay.Show(); - lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime; + lastPauseActionTime = GameplayClockContainer.CurrentTime; return true; } @@ -1005,7 +1005,7 @@ namespace osu.Game.Screens.Play /// protected virtual void StartGameplay() { - if (GameplayClockContainer.GameplayClock.IsRunning) + if (GameplayClockContainer.IsRunning) throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"); GameplayClockContainer.Reset(true); From c8764cb3336ffd3818d5df49f2d1e226770c0fd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 17:11:22 +0900 Subject: [PATCH 131/532] Move all usage of `GameplayClock` to `IGameplayClock` --- .../Default/SpinnerRotationTracker.cs | 2 +- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 2 +- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 2 +- .../TestSceneSkinEditorMultipleSkins.cs | 2 +- .../Gameplay/TestSceneSkinnableHUDOverlay.cs | 2 +- .../Rulesets/UI/FrameStabilityContainer.cs | 8 ++--- osu.Game/Screens/Play/ComboEffects.cs | 2 +- osu.Game/Screens/Play/GameplayClock.cs | 18 ++-------- .../Screens/Play/GameplayClockContainer.cs | 2 +- osu.Game/Screens/Play/HUD/SongProgress.cs | 2 +- osu.Game/Screens/Play/HUD/SongProgressInfo.cs | 4 +-- osu.Game/Screens/Play/IGameplayClock.cs | 34 +++++++++++++++++++ osu.Game/Screens/Play/SkipOverlay.cs | 2 +- .../Drawables/DrawableStoryboard.cs | 2 +- 14 files changed, 53 insertions(+), 31 deletions(-) create mode 100644 osu.Game/Screens/Play/IGameplayClock.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index f9ed8b8721..554ea3ac90 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private bool rotationTransferred; [Resolved(canBeNull: true)] - private GameplayClock gameplayClock { get; set; } + private IGameplayClock gameplayClock { get; set; } protected override void Update() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index d4f3d0f390..d6c49b026e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Gameplay (typeof(ScoreProcessor), actualComponentsContainer.Dependencies.Get()), (typeof(HealthProcessor), actualComponentsContainer.Dependencies.Get()), (typeof(GameplayState), actualComponentsContainer.Dependencies.Get()), - (typeof(GameplayClock), actualComponentsContainer.Dependencies.Get()) + (typeof(IGameplayClock), actualComponentsContainer.Dependencies.Get()) }, }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index fb97f94dbb..574f749e28 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached] - private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); // best way to check without exposing. private Drawable hideTarget => hudOverlay.KeyCounter; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index 3eb92b3e97..e694b396ad 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached] - private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index ee2827122d..6b990ce93c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached] - private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); private IEnumerable hudOverlays => CreatedDrawables.OfType(); diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index b4a0d83bf2..bfad22b4f0 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.UI public IFrameStableClock FrameStableClock => frameStableClock; - [Cached(typeof(GameplayClock))] + [Cached(typeof(IGameplayClock))] private readonly FrameStabilityClock frameStableClock; public FrameStabilityContainer(double gameplayStartTime = double.MinValue) @@ -64,12 +64,12 @@ namespace osu.Game.Rulesets.UI private int direction = 1; [BackgroundDependencyLoader(true)] - private void load(GameplayClock clock) + private void load(IGameplayClock clock) { if (clock != null) { parentGameplayClock = frameStableClock.ParentGameplayClock = clock; - frameStableClock.IsPaused.BindTo(clock.IsPaused); + ((IBindable)frameStableClock.IsPaused).BindTo(clock.IsPaused); } } @@ -272,7 +272,7 @@ namespace osu.Game.Rulesets.UI private class FrameStabilityClock : GameplayClock, IFrameStableClock { - public GameplayClock ParentGameplayClock; + public IGameplayClock ParentGameplayClock; public readonly Bindable IsCatchingUp = new Bindable(); diff --git a/osu.Game/Screens/Play/ComboEffects.cs b/osu.Game/Screens/Play/ComboEffects.cs index 77681401bb..442b061af7 100644 --- a/osu.Game/Screens/Play/ComboEffects.cs +++ b/osu.Game/Screens/Play/ComboEffects.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Play private ISamplePlaybackDisabler samplePlaybackDisabler { get; set; } [Resolved] - private GameplayClock gameplayClock { get; set; } + private IGameplayClock gameplayClock { get; set; } private void onComboChange(ValueChangedEvent combo) { diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index 6af795cfd8..454229fb31 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -19,15 +19,14 @@ namespace osu.Game.Screens.Play /// , as this should only be done once to ensure accuracy. /// /// - public class GameplayClock : IFrameBasedClock + public class GameplayClock : IGameplayClock { internal readonly IFrameBasedClock UnderlyingClock; public readonly BindableBool IsPaused = new BindableBool(); - /// - /// All adjustments applied to this clock which don't come from gameplay or mods. - /// + IBindable IGameplayClock.IsPaused => IsPaused; + public virtual IEnumerable> NonGameplayAdjustments => Enumerable.Empty>(); public GameplayClock(IFrameBasedClock underlyingClock) @@ -35,23 +34,12 @@ namespace osu.Game.Screens.Play UnderlyingClock = underlyingClock; } - /// - /// The time from which the clock should start. Will be seeked to on calling . - /// - /// - /// If not set, a value of zero will be used. - /// Importantly, the value will be inferred from the current ruleset in unless specified. - /// public double? StartTime { get; internal set; } public double CurrentTime => UnderlyingClock.CurrentTime; public double Rate => UnderlyingClock.Rate; - /// - /// The rate of gameplay when playback is at 100%. - /// This excludes any seeking / user adjustments. - /// public double TrueGameplayRate { get diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 2cc1fe8eaf..f7f115eddb 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Play { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableSource)); + dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableSource)); GameplayClock.StartTime = StartTime; GameplayClock.IsPaused.BindTo(IsPaused); diff --git a/osu.Game/Screens/Play/HUD/SongProgress.cs b/osu.Game/Screens/Play/HUD/SongProgress.cs index 85bb42193b..f368edbfb9 100644 --- a/osu.Game/Screens/Play/HUD/SongProgress.cs +++ b/osu.Game/Screens/Play/HUD/SongProgress.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Play.HUD public bool UsesFixedAnchor { get; set; } [Resolved] - protected GameplayClock GameplayClock { get; private set; } = null!; + protected IGameplayClock GameplayClock { get; private set; } = null!; [Resolved(canBeNull: true)] private DrawableRuleset? drawableRuleset { get; set; } diff --git a/osu.Game/Screens/Play/HUD/SongProgressInfo.cs b/osu.Game/Screens/Play/HUD/SongProgressInfo.cs index 8f10e84509..96a4c5f2bc 100644 --- a/osu.Game/Screens/Play/HUD/SongProgressInfo.cs +++ b/osu.Game/Screens/Play/HUD/SongProgressInfo.cs @@ -38,10 +38,10 @@ namespace osu.Game.Screens.Play.HUD set => endTime = value; } - private GameplayClock gameplayClock; + private IGameplayClock gameplayClock; [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, GameplayClock clock) + private void load(OsuColour colours, IGameplayClock clock) { if (clock != null) gameplayClock = clock; diff --git a/osu.Game/Screens/Play/IGameplayClock.cs b/osu.Game/Screens/Play/IGameplayClock.cs new file mode 100644 index 0000000000..c3d61be5d5 --- /dev/null +++ b/osu.Game/Screens/Play/IGameplayClock.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Framework.Timing; + +namespace osu.Game.Screens.Play +{ + public interface IGameplayClock : IFrameBasedClock + { + /// + /// The rate of gameplay when playback is at 100%. + /// This excludes any seeking / user adjustments. + /// + double TrueGameplayRate { get; } + + /// + /// The time from which the clock should start. Will be seeked to on calling . + /// + /// + /// If not set, a value of zero will be used. + /// Importantly, the value will be inferred from the current ruleset in unless specified. + /// + double? StartTime { get; } + + /// + /// All adjustments applied to this clock which don't come from gameplay or mods. + /// + IEnumerable> NonGameplayAdjustments { get; } + + IBindable IsPaused { get; } + } +} diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 3e2cf9a756..687705ff1b 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play private bool isClickable; [Resolved] - private GameplayClock gameplayClock { get; set; } + private IGameplayClock gameplayClock { get; set; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 8343f14050..6295604438 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -85,7 +85,7 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader(true)] - private void load(GameplayClock clock, CancellationToken? cancellationToken, GameHost host, RealmAccess realm) + private void load(IGameplayClock clock, CancellationToken? cancellationToken, GameHost host, RealmAccess realm) { if (clock != null) Clock = clock; From f81c7644b40b9348bcc92bad75ae1559e0e975f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 17:36:18 +0900 Subject: [PATCH 132/532] Make `GameplayClockContainer` also an `IGameplayClock` and expose to remaining tests --- .../Gameplay/TestSceneStoryboardSamples.cs | 2 -- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 2 +- .../TestSceneSkinEditorMultipleSkins.cs | 2 +- .../Gameplay/TestSceneSkinnableHUDOverlay.cs | 2 +- .../Visual/Gameplay/TestSceneSongProgress.cs | 2 +- osu.Game/Screens/Play/GameplayClockContainer.cs | 17 ++++++++++++----- osu.Game/Screens/Play/Player.cs | 2 +- .../Screens/Play/ScreenSuspensionHandler.cs | 2 +- 8 files changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 3ccf6c5d33..18ae2cb7c8 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -77,7 +77,6 @@ namespace osu.Game.Tests.Gameplay Add(gameplayContainer = new MasterGameplayClockContainer(working, 0) { - IsPaused = { Value = true }, Child = new FrameStabilityContainer { Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) @@ -106,7 +105,6 @@ namespace osu.Game.Tests.Gameplay Add(gameplayContainer = new MasterGameplayClockContainer(working, start_time) { StartTime = start_time, - IsPaused = { Value = true }, Child = new FrameStabilityContainer { Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 574f749e28..3e2698bc05 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); - [Cached] + [Cached(typeof(IGameplayClock))] private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); // best way to check without exposing. diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index e694b396ad..e29101ba8d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); - [Cached] + [Cached(typeof(IGameplayClock))] private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); [SetUpSteps] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 6b990ce93c..00e4171eac 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); - [Cached] + [Cached(typeof(IGameplayClock))] private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); private IEnumerable hudOverlays => CreatedDrawables.OfType(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs index 146cbfb052..3487f4dbff 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Gameplay Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)); - Dependencies.CacheAs(gameplayClockContainer); + Dependencies.CacheAs(gameplayClockContainer); } [SetUpSteps] diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index f7f115eddb..8400e0a9c2 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; @@ -15,12 +16,14 @@ namespace osu.Game.Screens.Play /// /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use. /// - public class GameplayClockContainer : Container, IAdjustableClock, IFrameBasedClock + public class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock { /// /// Whether gameplay is paused. /// - public readonly BindableBool IsPaused = new BindableBool(true); + public IBindable IsPaused => isPaused; + + private readonly BindableBool isPaused = new BindableBool(true); /// /// The adjustable source clock used for gameplay. Should be used for seeks and clock control. @@ -58,6 +61,8 @@ namespace osu.Game.Screens.Play } } + public IEnumerable> NonGameplayAdjustments => GameplayClock.NonGameplayAdjustments; + /// /// The final clock which is exposed to gameplay components. /// @@ -84,7 +89,7 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableSource)); GameplayClock.StartTime = StartTime; - GameplayClock.IsPaused.BindTo(IsPaused); + GameplayClock.IsPaused.BindTo(isPaused); return dependencies; } @@ -105,7 +110,7 @@ namespace osu.Game.Screens.Play AdjustableSource.Start(); } - IsPaused.Value = false; + isPaused.Value = false; } /// @@ -127,7 +132,7 @@ namespace osu.Game.Screens.Play /// /// Stops gameplay. /// - public void Stop() => IsPaused.Value = true; + public void Stop() => isPaused.Value = true; /// /// Resets this and the source to an initial state ready for gameplay. @@ -232,5 +237,7 @@ namespace osu.Game.Screens.Play public double FramesPerSecond => GameplayClock.FramesPerSecond; public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo; + + public double TrueGameplayRate => GameplayClock.TrueGameplayRate; } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 0ef09e4029..51b2042d1b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -330,7 +330,7 @@ namespace osu.Game.Screens.Play DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState()); // bind clock into components that require it - DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); + ((IBindable)DrawableRuleset.IsPaused).BindTo(GameplayClockContainer.IsPaused); DrawableRuleset.NewResult += r => { diff --git a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs index 59b92a1b97..cc1254975c 100644 --- a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs +++ b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play public class ScreenSuspensionHandler : Component { private readonly GameplayClockContainer gameplayClockContainer; - private Bindable isPaused; + private IBindable isPaused; private readonly Bindable disableSuspensionBindable = new Bindable(); From c5f8529d20a57f198a4fb6be7c68090cef7c4fe3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 17:57:26 +0900 Subject: [PATCH 133/532] Mark unused methods as `NotImplemented` for safety --- osu.Game/Screens/Play/GameplayClockContainer.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 8400e0a9c2..bf689dcfe1 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -209,9 +209,7 @@ namespace osu.Game.Screens.Play void IAdjustableClock.Reset() => Reset(); - public void ResetSpeedAdjustments() - { - } + public void ResetSpeedAdjustments() => throw new NotImplementedException(); double IAdjustableClock.Rate { From cc982d374cf569d645051e87249a47a46b45b2f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 18:14:57 +0900 Subject: [PATCH 134/532] Cache self rather than `GameplayClock` --- osu.Game/Screens/Play/GameplayClockContainer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index bf689dcfe1..df1eb32f99 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -86,7 +86,9 @@ namespace osu.Game.Screens.Play { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableSource)); + GameplayClock = CreateGameplayClock(AdjustableSource); + + dependencies.CacheAs(this); GameplayClock.StartTime = StartTime; GameplayClock.IsPaused.BindTo(isPaused); From 27569e2ed5f0006d9066a05cf2f5d44cb703bc8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 18:53:10 +0900 Subject: [PATCH 135/532] Remove `FrameStableClock` (and redirect usages to `FrameStabilityContainer`) --- osu.Game.Tests/NonVisual/GameplayClockTest.cs | 3 +- .../Visual/Gameplay/TestSceneSpectator.cs | 2 +- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 +- .../Rulesets/UI/FrameStabilityContainer.cs | 97 ++++++++++--------- osu.Game/Rulesets/UI/IFrameStableClock.cs | 2 - osu.Game/Screens/Play/GameplayClock.cs | 8 +- .../Screens/Play/GameplayClockContainer.cs | 2 +- osu.Game/Screens/Play/IGameplayClock.cs | 2 +- .../Play/MasterGameplayClockContainer.cs | 2 +- 9 files changed, 63 insertions(+), 57 deletions(-) diff --git a/osu.Game.Tests/NonVisual/GameplayClockTest.cs b/osu.Game.Tests/NonVisual/GameplayClockTest.cs index 5b8aacd281..162734f9da 100644 --- a/osu.Game.Tests/NonVisual/GameplayClockTest.cs +++ b/osu.Game.Tests/NonVisual/GameplayClockTest.cs @@ -4,6 +4,7 @@ #nullable disable using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Timing; @@ -30,7 +31,7 @@ namespace osu.Game.Tests.NonVisual { public List> MutableNonGameplayAdjustments { get; } = new List>(); - public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; + public override IEnumerable NonGameplayAdjustments => MutableNonGameplayAdjustments.Select(b => b.Value); public TestGameplayClock(IFrameBasedClock underlyingClock) : base(underlyingClock) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index a42e86933f..0aa412a4fd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -363,7 +363,7 @@ namespace osu.Game.Tests.Visual.Gameplay private Player player => Stack.CurrentScreen as Player; private double currentFrameStableTime - => player.ChildrenOfType().First().FrameStableClock.CurrentTime; + => player.ChildrenOfType().First().CurrentTime; private void waitForPlayer() => AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index f7f62d2af0..59c1146995 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.UI public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both }; - public override IFrameStableClock FrameStableClock => frameStabilityContainer.FrameStableClock; + public override IFrameStableClock FrameStableClock => frameStabilityContainer; private bool frameStablePlayback = true; diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index bfad22b4f0..7b7003302a 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Diagnostics; @@ -21,7 +19,9 @@ namespace osu.Game.Rulesets.UI /// A container which consumes a parent gameplay clock and standardises frame counts for children. /// Will ensure a minimum of 50 frames per clock second is maintained, regardless of any system lag or seeks. /// - public class FrameStabilityContainer : Container, IHasReplayHandler + [Cached(typeof(IGameplayClock))] + [Cached(typeof(IFrameStableClock))] + public class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock, IGameplayClock { private readonly double gameplayStartTime; @@ -35,16 +35,17 @@ namespace osu.Game.Rulesets.UI /// internal bool FrameStablePlayback = true; - public IFrameStableClock FrameStableClock => frameStableClock; + public readonly Bindable IsCatchingUp = new Bindable(); - [Cached(typeof(IGameplayClock))] - private readonly FrameStabilityClock frameStableClock; + public readonly Bindable WaitingOnFrames = new Bindable(); + + public IBindable IsPaused { get; } = new BindableBool(); public FrameStabilityContainer(double gameplayStartTime = double.MinValue) { RelativeSizeAxes = Axes.Both; - frameStableClock = new FrameStabilityClock(framedClock = new FramedClock(manualClock = new ManualClock())); + framedClock = new FramedClock(manualClock = new ManualClock()); this.gameplayStartTime = gameplayStartTime; } @@ -53,7 +54,7 @@ namespace osu.Game.Rulesets.UI private readonly FramedClock framedClock; - private IFrameBasedClock parentGameplayClock; + private IGameplayClock? parentGameplayClock; /// /// The current direction of playback to be exposed to frame stable children. @@ -63,13 +64,13 @@ namespace osu.Game.Rulesets.UI /// private int direction = 1; - [BackgroundDependencyLoader(true)] - private void load(IGameplayClock clock) + [BackgroundDependencyLoader] + private void load(IGameplayClock? clock) { if (clock != null) { - parentGameplayClock = frameStableClock.ParentGameplayClock = clock; - ((IBindable)frameStableClock.IsPaused).BindTo(clock.IsPaused); + parentGameplayClock = clock; + IsPaused.BindTo(parentGameplayClock.IsPaused); } } @@ -111,12 +112,12 @@ namespace osu.Game.Rulesets.UI private void updateClock() { - if (frameStableClock.WaitingOnFrames.Value) + if (WaitingOnFrames.Value) { // if waiting on frames, run one update loop to determine if frames have arrived. state = PlaybackState.Valid; } - else if (frameStableClock.IsPaused.Value) + else if (IsPaused.Value) { // time should not advance while paused, nor should anything run. state = PlaybackState.NotValid; @@ -154,8 +155,8 @@ namespace osu.Game.Rulesets.UI double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime); - frameStableClock.IsCatchingUp.Value = timeBehind > 200; - frameStableClock.WaitingOnFrames.Value = state == PlaybackState.NotValid; + IsCatchingUp.Value = timeBehind > 200; + WaitingOnFrames.Value = state == PlaybackState.NotValid; manualClock.CurrentTime = proposedTime; manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction; @@ -177,6 +178,8 @@ namespace osu.Game.Rulesets.UI /// Whether playback is still valid. private bool updateReplay(ref double proposedTime) { + Debug.Assert(ReplayInputHandler != null); + double? newTime; if (FrameStablePlayback) @@ -238,18 +241,39 @@ namespace osu.Game.Rulesets.UI private void setClock() { - if (parentGameplayClock == null) - { - // in case a parent gameplay clock isn't available, just use the parent clock. - parentGameplayClock ??= Clock; - } - else - { - Clock = frameStableClock; - } + if (parentGameplayClock != null) + Clock = this; } - public ReplayInputHandler ReplayInputHandler { get; set; } + public ReplayInputHandler? ReplayInputHandler { get; set; } + + #region Delegation of IFrameStableClock + + public double CurrentTime => framedClock.CurrentTime; + + public double Rate => framedClock.Rate; + + public bool IsRunning => framedClock.IsRunning; + + public void ProcessFrame() => framedClock.ProcessFrame(); + + public double ElapsedFrameTime => framedClock.ElapsedFrameTime; + + public double FramesPerSecond => framedClock.FramesPerSecond; + + public FrameTimeInfo TimeInfo => framedClock.TimeInfo; + + #endregion + + #region Delegation of IGameplayClock + + public double TrueGameplayRate => parentGameplayClock?.TrueGameplayRate ?? 1; + + public double? StartTime => parentGameplayClock?.StartTime; + + public IEnumerable NonGameplayAdjustments => parentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty(); + + #endregion private enum PlaybackState { @@ -270,24 +294,7 @@ namespace osu.Game.Rulesets.UI Valid } - private class FrameStabilityClock : GameplayClock, IFrameStableClock - { - public IGameplayClock ParentGameplayClock; - - public readonly Bindable IsCatchingUp = new Bindable(); - - public readonly Bindable WaitingOnFrames = new Bindable(); - - public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty>(); - - public FrameStabilityClock(FramedClock underlyingClock) - : base(underlyingClock) - { - } - - IBindable IFrameStableClock.IsCatchingUp => IsCatchingUp; - - IBindable IFrameStableClock.WaitingOnFrames => WaitingOnFrames; - } + IBindable IFrameStableClock.IsCatchingUp => IsCatchingUp; + IBindable IFrameStableClock.WaitingOnFrames => WaitingOnFrames; } } diff --git a/osu.Game/Rulesets/UI/IFrameStableClock.cs b/osu.Game/Rulesets/UI/IFrameStableClock.cs index 132605adaf..569ef5e06c 100644 --- a/osu.Game/Rulesets/UI/IFrameStableClock.cs +++ b/osu.Game/Rulesets/UI/IFrameStableClock.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Timing; diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index 454229fb31..b650922173 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Play IBindable IGameplayClock.IsPaused => IsPaused; - public virtual IEnumerable> NonGameplayAdjustments => Enumerable.Empty>(); + public virtual IEnumerable NonGameplayAdjustments => Enumerable.Empty(); public GameplayClock(IFrameBasedClock underlyingClock) { @@ -46,12 +46,12 @@ namespace osu.Game.Screens.Play { double baseRate = Rate; - foreach (var adjustment in NonGameplayAdjustments) + foreach (double adjustment in NonGameplayAdjustments) { - if (Precision.AlmostEquals(adjustment.Value, 0)) + if (Precision.AlmostEquals(adjustment, 0)) return 0; - baseRate /= adjustment.Value; + baseRate /= adjustment; } return baseRate; diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index df1eb32f99..27b37094ad 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Play } } - public IEnumerable> NonGameplayAdjustments => GameplayClock.NonGameplayAdjustments; + public IEnumerable NonGameplayAdjustments => GameplayClock.NonGameplayAdjustments; /// /// The final clock which is exposed to gameplay components. diff --git a/osu.Game/Screens/Play/IGameplayClock.cs b/osu.Game/Screens/Play/IGameplayClock.cs index c3d61be5d5..5f54ce691a 100644 --- a/osu.Game/Screens/Play/IGameplayClock.cs +++ b/osu.Game/Screens/Play/IGameplayClock.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Play /// /// All adjustments applied to this clock which don't come from gameplay or mods. /// - IEnumerable> NonGameplayAdjustments { get; } + IEnumerable NonGameplayAdjustments { get; } IBindable IsPaused { get; } } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index d7f6992fee..587d2d40a1 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -303,7 +303,7 @@ namespace osu.Game.Screens.Play private class MasterGameplayClock : GameplayClock { public readonly List> MutableNonGameplayAdjustments = new List>(); - public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; + public override IEnumerable NonGameplayAdjustments => MutableNonGameplayAdjustments.Select(b => b.Value); public MasterGameplayClock(FramedOffsetClock underlyingClock) : base(underlyingClock) From 828b6f2c30b779d4cee1d10c798c3b9a1353d305 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 19:01:28 +0900 Subject: [PATCH 136/532] Remove unnecessary `setClock` shenanigans --- .../Rulesets/UI/FrameStabilityContainer.cs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 7b7003302a..c115a0b6ac 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.UI protected override void LoadComplete() { base.LoadComplete(); - setClock(); + Clock = this; } private PlaybackState state; @@ -128,12 +128,7 @@ namespace osu.Game.Rulesets.UI state = PlaybackState.Valid; } - if (parentGameplayClock == null) - setClock(); // LoadComplete may not be run yet, but we still want the clock. - - Debug.Assert(parentGameplayClock != null); - - double proposedTime = parentGameplayClock.CurrentTime; + double proposedTime = Clock.CurrentTime; if (FrameStablePlayback) // if we require frame stability, the proposed time will be adjusted to move at most one known @@ -153,14 +148,14 @@ namespace osu.Game.Rulesets.UI if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime) direction = proposedTime >= manualClock.CurrentTime ? 1 : -1; - double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime); + double timeBehind = Math.Abs(proposedTime - Clock.CurrentTime); IsCatchingUp.Value = timeBehind > 200; WaitingOnFrames.Value = state == PlaybackState.NotValid; manualClock.CurrentTime = proposedTime; - manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction; - manualClock.IsRunning = parentGameplayClock.IsRunning; + manualClock.Rate = Math.Abs(Clock.Rate) * direction; + manualClock.IsRunning = Clock.IsRunning; // determine whether catch-up is required. if (state == PlaybackState.Valid && timeBehind > 0) @@ -239,12 +234,6 @@ namespace osu.Game.Rulesets.UI } } - private void setClock() - { - if (parentGameplayClock != null) - Clock = this; - } - public ReplayInputHandler? ReplayInputHandler { get; set; } #region Delegation of IFrameStableClock From 04d88b82163a9cf3895073784bfd53fb797451ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 19:17:22 +0900 Subject: [PATCH 137/532] Use constraint based assertions in `TestSceneFrameStabilityContainer` --- .../Visual/Gameplay/TestSceneFrameStabilityContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs index 97ffbfc796..ef74024b4b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs @@ -137,13 +137,13 @@ namespace osu.Game.Tests.Visual.Gameplay private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time); - private void confirmSeek(double time) => AddUntilStep($"wait for seek to {time}", () => consumer.Clock.CurrentTime == time); + private void confirmSeek(double time) => AddUntilStep($"wait for seek to {time}", () => consumer.Clock.CurrentTime, () => Is.EqualTo(time)); private void checkFrameCount(int frames) => - AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == frames); + AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames, () => Is.EqualTo(frames)); private void checkRate(double rate) => - AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate == rate); + AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate, () => Is.EqualTo(rate)); public class ClockConsumingChild : CompositeDrawable { From 9bc2e91de01864e16711765869e264ade0cd1ae0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 19:17:41 +0900 Subject: [PATCH 138/532] Fix incorrect handling of reference clocks when no parent `IGameplayClock` is available --- .../Rulesets/UI/FrameStabilityContainer.cs | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index c115a0b6ac..411217e314 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; +using osu.Framework.Utils; using osu.Game.Input.Handlers; using osu.Game.Screens.Play; @@ -56,6 +57,8 @@ namespace osu.Game.Rulesets.UI private IGameplayClock? parentGameplayClock; + private IClock referenceClock = null!; + /// /// The current direction of playback to be exposed to frame stable children. /// @@ -65,18 +68,15 @@ namespace osu.Game.Rulesets.UI private int direction = 1; [BackgroundDependencyLoader] - private void load(IGameplayClock? clock) + private void load(IGameplayClock? gameplayClock) { - if (clock != null) + if (gameplayClock != null) { - parentGameplayClock = clock; + parentGameplayClock = gameplayClock; IsPaused.BindTo(parentGameplayClock.IsPaused); } - } - protected override void LoadComplete() - { - base.LoadComplete(); + referenceClock = gameplayClock ?? Clock; Clock = this; } @@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.UI state = PlaybackState.Valid; } - double proposedTime = Clock.CurrentTime; + double proposedTime = referenceClock.CurrentTime; if (FrameStablePlayback) // if we require frame stability, the proposed time will be adjusted to move at most one known @@ -148,14 +148,14 @@ namespace osu.Game.Rulesets.UI if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime) direction = proposedTime >= manualClock.CurrentTime ? 1 : -1; - double timeBehind = Math.Abs(proposedTime - Clock.CurrentTime); + double timeBehind = Math.Abs(proposedTime - CurrentTime); IsCatchingUp.Value = timeBehind > 200; WaitingOnFrames.Value = state == PlaybackState.NotValid; manualClock.CurrentTime = proposedTime; - manualClock.Rate = Math.Abs(Clock.Rate) * direction; - manualClock.IsRunning = Clock.IsRunning; + manualClock.Rate = Math.Abs(referenceClock.Rate) * direction; + manualClock.IsRunning = referenceClock.IsRunning; // determine whether catch-up is required. if (state == PlaybackState.Valid && timeBehind > 0) @@ -244,7 +244,7 @@ namespace osu.Game.Rulesets.UI public bool IsRunning => framedClock.IsRunning; - public void ProcessFrame() => framedClock.ProcessFrame(); + public void ProcessFrame() { } public double ElapsedFrameTime => framedClock.ElapsedFrameTime; @@ -256,7 +256,23 @@ namespace osu.Game.Rulesets.UI #region Delegation of IGameplayClock - public double TrueGameplayRate => parentGameplayClock?.TrueGameplayRate ?? 1; + public double TrueGameplayRate + { + get + { + double baseRate = Rate; + + foreach (double adjustment in NonGameplayAdjustments) + { + if (Precision.AlmostEquals(adjustment, 0)) + return 0; + + baseRate /= adjustment; + } + + return baseRate; + } + } public double? StartTime => parentGameplayClock?.StartTime; From fff2b57905d75b0cb6d57f7183202e0fa80a2a4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 19:28:12 +0900 Subject: [PATCH 139/532] Tidy up and document `FrameStabilityContainer` --- .../Rulesets/UI/FrameStabilityContainer.cs | 97 +++++++++++-------- 1 file changed, 54 insertions(+), 43 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 411217e314..aa21b03b9d 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.UI [Cached(typeof(IFrameStableClock))] public class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock, IGameplayClock { - private readonly double gameplayStartTime; + public ReplayInputHandler? ReplayInputHandler { get; set; } /// /// The number of frames (per parent frame) which can be run in an attempt to catch-up to real-time. @@ -34,13 +34,48 @@ namespace osu.Game.Rulesets.UI /// /// Whether to enable frame-stable playback. /// - internal bool FrameStablePlayback = true; + internal bool FrameStablePlayback { get; set; } = true; - public readonly Bindable IsCatchingUp = new Bindable(); + protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && state != PlaybackState.NotValid; - public readonly Bindable WaitingOnFrames = new Bindable(); + private readonly Bindable isCatchingUp = new Bindable(); - public IBindable IsPaused { get; } = new BindableBool(); + private readonly Bindable waitingOnFrames = new Bindable(); + + private readonly double gameplayStartTime; + + private IGameplayClock? parentGameplayClock; + + /// + /// A clock which is used as reference for time, rate and running state. + /// + private IClock referenceClock = null!; + + /// + /// A local manual clock which tracks the reference clock. + /// Values are transferred from each update call. + /// + private readonly ManualClock manualClock; + + /// + /// The main framed clock which has stability applied to it. + /// This gets exposed to children as an . + /// + private readonly FramedClock framedClock; + + /// + /// The current direction of playback to be exposed to frame stable children. + /// + /// + /// Initially it is presumed that playback will proceed in the forward direction. + /// + private int direction = 1; + + private PlaybackState state; + + private bool hasReplayAttached => ReplayInputHandler != null; + + private bool firstConsumption = true; public FrameStabilityContainer(double gameplayStartTime = double.MinValue) { @@ -51,22 +86,6 @@ namespace osu.Game.Rulesets.UI this.gameplayStartTime = gameplayStartTime; } - private readonly ManualClock manualClock; - - private readonly FramedClock framedClock; - - private IGameplayClock? parentGameplayClock; - - private IClock referenceClock = null!; - - /// - /// The current direction of playback to be exposed to frame stable children. - /// - /// - /// Initially it is presumed that playback will proceed in the forward direction. - /// - private int direction = 1; - [BackgroundDependencyLoader] private void load(IGameplayClock? gameplayClock) { @@ -80,16 +99,6 @@ namespace osu.Game.Rulesets.UI Clock = this; } - private PlaybackState state; - - protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && state != PlaybackState.NotValid; - - private bool hasReplayAttached => ReplayInputHandler != null; - - private const double sixty_frame_time = 1000.0 / 60; - - private bool firstConsumption = true; - public override bool UpdateSubTree() { int loops = MaxCatchUpFrames; @@ -112,7 +121,7 @@ namespace osu.Game.Rulesets.UI private void updateClock() { - if (WaitingOnFrames.Value) + if (waitingOnFrames.Value) { // if waiting on frames, run one update loop to determine if frames have arrived. state = PlaybackState.Valid; @@ -150,8 +159,8 @@ namespace osu.Game.Rulesets.UI double timeBehind = Math.Abs(proposedTime - CurrentTime); - IsCatchingUp.Value = timeBehind > 200; - WaitingOnFrames.Value = state == PlaybackState.NotValid; + isCatchingUp.Value = timeBehind > 200; + waitingOnFrames.Value = state == PlaybackState.NotValid; manualClock.CurrentTime = proposedTime; manualClock.Rate = Math.Abs(referenceClock.Rate) * direction; @@ -211,6 +220,8 @@ namespace osu.Game.Rulesets.UI /// The time which is to be displayed. private void applyFrameStability(ref double proposedTime) { + const double sixty_frame_time = 1000.0 / 60; + if (firstConsumption) { // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour. @@ -234,9 +245,9 @@ namespace osu.Game.Rulesets.UI } } - public ReplayInputHandler? ReplayInputHandler { get; set; } + #region Delegation of IGameplayClock - #region Delegation of IFrameStableClock + public IBindable IsPaused { get; } = new BindableBool(); public double CurrentTime => framedClock.CurrentTime; @@ -252,10 +263,6 @@ namespace osu.Game.Rulesets.UI public FrameTimeInfo TimeInfo => framedClock.TimeInfo; - #endregion - - #region Delegation of IGameplayClock - public double TrueGameplayRate { get @@ -280,6 +287,13 @@ namespace osu.Game.Rulesets.UI #endregion + #region Delegation of IFrameStableClock + + IBindable IFrameStableClock.IsCatchingUp => isCatchingUp; + IBindable IFrameStableClock.WaitingOnFrames => waitingOnFrames; + + #endregion + private enum PlaybackState { /// @@ -298,8 +312,5 @@ namespace osu.Game.Rulesets.UI /// Valid } - - IBindable IFrameStableClock.IsCatchingUp => IsCatchingUp; - IBindable IFrameStableClock.WaitingOnFrames => WaitingOnFrames; } } From 1fc3d005c072d3d41c493de4a47e437fd544aa3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 19:31:01 +0900 Subject: [PATCH 140/532] Seal `FrameStabilityContainer` No one should ever derive from this class. It is already too complex. --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index aa21b03b9d..cdb2157c4a 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.UI /// [Cached(typeof(IGameplayClock))] [Cached(typeof(IFrameStableClock))] - public class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock, IGameplayClock + public sealed class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock, IGameplayClock { public ReplayInputHandler? ReplayInputHandler { get; set; } From 87760bbc066b7e30b0b4a2986acd7f452a53b664 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 20:16:14 +0900 Subject: [PATCH 141/532] Fix `IsCatchingUp` not being in correct state --- .../Gameplay/TestSceneGameplaySamplePlayback.cs | 12 +++++------- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index f1084bca5f..1fe2dfd4df 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -21,22 +19,22 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestAllSamplesStopDuringSeek() { - DrawableSlider slider = null; - PoolableSkinnableSample[] samples = null; - ISamplePlaybackDisabler sampleDisabler = null; + DrawableSlider? slider = null; + PoolableSkinnableSample[] samples = null!; + ISamplePlaybackDisabler sampleDisabler = null!; AddUntilStep("get variables", () => { sampleDisabler = Player; slider = Player.ChildrenOfType().MinBy(s => s.HitObject.StartTime); - samples = slider?.ChildrenOfType().ToArray(); + samples = slider.ChildrenOfType().ToArray(); return slider != null; }); AddUntilStep("wait for slider sliding then seek", () => { - if (!slider.Tracking.Value) + if (slider?.Tracking.Value != true) return false; if (!samples.Any(s => s.Playing)) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 411217e314..e75e5cc5f3 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.UI if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime) direction = proposedTime >= manualClock.CurrentTime ? 1 : -1; - double timeBehind = Math.Abs(proposedTime - CurrentTime); + double timeBehind = Math.Abs(proposedTime - referenceClock.CurrentTime); IsCatchingUp.Value = timeBehind > 200; WaitingOnFrames.Value = state == PlaybackState.NotValid; From 704568ae3b099a693b53c2c81703b77aa057a16e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 19:46:29 +0900 Subject: [PATCH 142/532] Remove remaining usage of `GameplayClock` --- osu.Game.Tests/NonVisual/GameplayClockTest.cs | 16 +--- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 2 +- .../TestSceneSkinEditorMultipleSkins.cs | 2 +- .../Gameplay/TestSceneSkinnableHUDOverlay.cs | 2 +- osu.Game/Screens/Play/GameplayClock.cs | 76 ------------------- .../Screens/Play/GameplayClockContainer.cs | 48 ++++++------ .../Play/MasterGameplayClockContainer.cs | 30 +++----- 7 files changed, 42 insertions(+), 134 deletions(-) delete mode 100644 osu.Game/Screens/Play/GameplayClock.cs diff --git a/osu.Game.Tests/NonVisual/GameplayClockTest.cs b/osu.Game.Tests/NonVisual/GameplayClockTest.cs index 162734f9da..9854a5731e 100644 --- a/osu.Game.Tests/NonVisual/GameplayClockTest.cs +++ b/osu.Game.Tests/NonVisual/GameplayClockTest.cs @@ -1,12 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; -using System.Linq; using NUnit.Framework; -using osu.Framework.Bindables; using osu.Framework.Timing; using osu.Game.Screens.Play; @@ -20,20 +16,16 @@ namespace osu.Game.Tests.NonVisual public void TestTrueGameplayRateWithZeroAdjustment(double underlyingClockRate) { var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate }); - var gameplayClock = new TestGameplayClock(framedClock); - - gameplayClock.MutableNonGameplayAdjustments.Add(new BindableDouble()); + var gameplayClock = new TestGameplayClockContainer(framedClock); Assert.That(gameplayClock.TrueGameplayRate, Is.EqualTo(0)); } - private class TestGameplayClock : GameplayClock + private class TestGameplayClockContainer : GameplayClockContainer { - public List> MutableNonGameplayAdjustments { get; } = new List>(); + public override IEnumerable NonGameplayAdjustments => new[] { 0.0 }; - public override IEnumerable NonGameplayAdjustments => MutableNonGameplayAdjustments.Select(b => b.Value); - - public TestGameplayClock(IFrameBasedClock underlyingClock) + public TestGameplayClockContainer(IFrameBasedClock underlyingClock) : base(underlyingClock) { } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 3e2698bc05..da6604a653 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached(typeof(IGameplayClock))] - private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock()); // best way to check without exposing. private Drawable hideTarget => hudOverlay.KeyCounter; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index e29101ba8d..6c02ddab14 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached(typeof(IGameplayClock))] - private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock()); [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 00e4171eac..485c76ac5c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached(typeof(IGameplayClock))] - private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock()); private IEnumerable hudOverlays => CreatedDrawables.OfType(); diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs deleted file mode 100644 index b650922173..0000000000 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Bindables; -using osu.Framework.Timing; -using osu.Framework.Utils; - -namespace osu.Game.Screens.Play -{ - /// - /// A clock which is used for gameplay elements that need to follow audio time 1:1. - /// Exposed via DI by . - /// - /// The main purpose of this clock is to stop components using it from accidentally processing the main - /// , as this should only be done once to ensure accuracy. - /// - /// - public class GameplayClock : IGameplayClock - { - internal readonly IFrameBasedClock UnderlyingClock; - - public readonly BindableBool IsPaused = new BindableBool(); - - IBindable IGameplayClock.IsPaused => IsPaused; - - public virtual IEnumerable NonGameplayAdjustments => Enumerable.Empty(); - - public GameplayClock(IFrameBasedClock underlyingClock) - { - UnderlyingClock = underlyingClock; - } - - public double? StartTime { get; internal set; } - - public double CurrentTime => UnderlyingClock.CurrentTime; - - public double Rate => UnderlyingClock.Rate; - - public double TrueGameplayRate - { - get - { - double baseRate = Rate; - - foreach (double adjustment in NonGameplayAdjustments) - { - if (Precision.AlmostEquals(adjustment, 0)) - return 0; - - baseRate /= adjustment; - } - - return baseRate; - } - } - - public bool IsRunning => UnderlyingClock.IsRunning; - - public void ProcessFrame() - { - // intentionally not updating the underlying clock (handled externally). - } - - public double ElapsedFrameTime => UnderlyingClock.ElapsedFrameTime; - - public double FramesPerSecond => UnderlyingClock.FramesPerSecond; - - public FrameTimeInfo TimeInfo => UnderlyingClock.TimeInfo; - - public IClock Source => UnderlyingClock; - } -} diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 27b37094ad..468e172714 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -3,13 +3,14 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Timing; +using osu.Framework.Utils; namespace osu.Game.Screens.Play { @@ -40,8 +41,6 @@ namespace osu.Game.Screens.Play /// public event Action? OnSeek; - private double? startTime; - /// /// The time from which the clock should start. Will be seeked to on calling . /// @@ -49,24 +48,14 @@ namespace osu.Game.Screens.Play /// If not set, a value of zero will be used. /// Importantly, the value will be inferred from the current ruleset in unless specified. /// - public double? StartTime - { - get => startTime; - set - { - startTime = value; + public double? StartTime { get; set; } - if (GameplayClock.IsNotNull()) - GameplayClock.StartTime = value; - } - } - - public IEnumerable NonGameplayAdjustments => GameplayClock.NonGameplayAdjustments; + public virtual IEnumerable NonGameplayAdjustments => Enumerable.Empty(); /// /// The final clock which is exposed to gameplay components. /// - protected GameplayClock GameplayClock { get; private set; } = null!; + protected IFrameBasedClock GameplayClock { get; private set; } = null!; /// /// Creates a new . @@ -90,9 +79,6 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(this); - GameplayClock.StartTime = StartTime; - GameplayClock.IsPaused.BindTo(isPaused); - return dependencies; } @@ -126,7 +112,7 @@ namespace osu.Game.Screens.Play AdjustableSource.Seek(time); // Manually process to make sure the gameplay clock is correctly updated after a seek. - GameplayClock.UnderlyingClock.ProcessFrame(); + GameplayClock.ProcessFrame(); OnSeek?.Invoke(); } @@ -174,7 +160,7 @@ namespace osu.Game.Screens.Play protected override void Update() { if (!IsPaused.Value) - GameplayClock.UnderlyingClock.ProcessFrame(); + GameplayClock.ProcessFrame(); base.Update(); } @@ -199,7 +185,7 @@ namespace osu.Game.Screens.Play /// /// The providing the source time. /// The final . - protected virtual GameplayClock CreateGameplayClock(IFrameBasedClock source) => new GameplayClock(source); + protected virtual IFrameBasedClock CreateGameplayClock(IFrameBasedClock source) => source; #region IAdjustableClock @@ -238,6 +224,22 @@ namespace osu.Game.Screens.Play public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo; - public double TrueGameplayRate => GameplayClock.TrueGameplayRate; + public double TrueGameplayRate + { + get + { + double baseRate = Rate; + + foreach (double adjustment in NonGameplayAdjustments) + { + if (Precision.AlmostEquals(adjustment, 0)) + return 0; + + baseRate /= adjustment; + } + + return baseRate; + } + } } } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 587d2d40a1..0427792392 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -54,7 +54,6 @@ namespace osu.Game.Screens.Play private HardwareCorrectionOffsetClock userGlobalOffsetClock = null!; private HardwareCorrectionOffsetClock userBeatmapOffsetClock = null!; private HardwareCorrectionOffsetClock platformOffsetClock = null!; - private MasterGameplayClock masterGameplayClock = null!; private Bindable userAudioOffset = null!; private IDisposable? beatmapOffsetSubscription; @@ -150,7 +149,7 @@ namespace osu.Game.Screens.Play // We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment. // Without doing this, an initial seek may be performed with the wrong offset. - GameplayClock.UnderlyingClock.ProcessFrame(); + GameplayClock.ProcessFrame(); } } @@ -191,7 +190,7 @@ namespace osu.Game.Screens.Play Seek(skipTarget); } - protected override GameplayClock CreateGameplayClock(IFrameBasedClock source) + protected override IFrameBasedClock CreateGameplayClock(IFrameBasedClock source) { // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. @@ -199,9 +198,7 @@ namespace osu.Game.Screens.Play // the final usable gameplay clock with user-set offsets applied. userGlobalOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock, pauseFreqAdjust); - userBeatmapOffsetClock = new HardwareCorrectionOffsetClock(userGlobalOffsetClock, pauseFreqAdjust); - - return masterGameplayClock = new MasterGameplayClock(userBeatmapOffsetClock); + return userBeatmapOffsetClock = new HardwareCorrectionOffsetClock(userGlobalOffsetClock, pauseFreqAdjust); } /// @@ -224,8 +221,8 @@ namespace osu.Game.Screens.Play Track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); Track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - masterGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust); - masterGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate); + nonGameplayAdjustments.Add(pauseFreqAdjust); + nonGameplayAdjustments.Add(UserPlaybackRate); speedAdjustmentsApplied = true; } @@ -238,8 +235,8 @@ namespace osu.Game.Screens.Play Track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); Track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - masterGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust); - masterGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate); + nonGameplayAdjustments.Remove(pauseFreqAdjust); + nonGameplayAdjustments.Remove(UserPlaybackRate); speedAdjustmentsApplied = false; } @@ -252,7 +249,7 @@ namespace osu.Game.Screens.Play } ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo; - IClock IBeatSyncProvider.Clock => GameplayClock; + IClock IBeatSyncProvider.Clock => this; ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => beatmap.TrackLoaded ? beatmap.Track.CurrentAmplitudes : ChannelAmplitudes.Empty; private class HardwareCorrectionOffsetClock : FramedOffsetClock @@ -300,15 +297,8 @@ namespace osu.Game.Screens.Play } } - private class MasterGameplayClock : GameplayClock - { - public readonly List> MutableNonGameplayAdjustments = new List>(); - public override IEnumerable NonGameplayAdjustments => MutableNonGameplayAdjustments.Select(b => b.Value); + private readonly List> nonGameplayAdjustments = new List>(); - public MasterGameplayClock(FramedOffsetClock underlyingClock) - : base(underlyingClock) - { - } - } + public override IEnumerable NonGameplayAdjustments => nonGameplayAdjustments.Select(b => b.Value); } } From 1696a905bace1db3a55b919fe503dc1a24d0d481 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 19:59:08 +0900 Subject: [PATCH 143/532] Reduce exposed properties in `GameplayClockContainer` --- ...kTest.cs => GameplayClockContainerTest.cs} | 2 +- .../Screens/Play/GameplayClockContainer.cs | 68 +++++++++---------- .../Play/MasterGameplayClockContainer.cs | 26 ++++--- 3 files changed, 50 insertions(+), 46 deletions(-) rename osu.Game.Tests/NonVisual/{GameplayClockTest.cs => GameplayClockContainerTest.cs} (96%) diff --git a/osu.Game.Tests/NonVisual/GameplayClockTest.cs b/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs similarity index 96% rename from osu.Game.Tests/NonVisual/GameplayClockTest.cs rename to osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs index 9854a5731e..f9f4ead644 100644 --- a/osu.Game.Tests/NonVisual/GameplayClockTest.cs +++ b/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs @@ -9,7 +9,7 @@ using osu.Game.Screens.Play; namespace osu.Game.Tests.NonVisual { [TestFixture] - public class GameplayClockTest + public class GameplayClockContainerTest { [TestCase(0)] [TestCase(1)] diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 468e172714..a5d6cbf2e1 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -15,7 +15,7 @@ using osu.Framework.Utils; namespace osu.Game.Screens.Play { /// - /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use. + /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use. /// public class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock { @@ -24,15 +24,8 @@ namespace osu.Game.Screens.Play /// public IBindable IsPaused => isPaused; - private readonly BindableBool isPaused = new BindableBool(true); - /// - /// The adjustable source clock used for gameplay. Should be used for seeks and clock control. - /// - protected readonly DecoupleableInterpolatingFramedClock AdjustableSource; - - /// - /// The source clock. + /// The source clock. Should generally not be used for any timekeeping purposes. /// public IClock SourceClock { get; private set; } @@ -55,7 +48,14 @@ namespace osu.Game.Screens.Play /// /// The final clock which is exposed to gameplay components. /// - protected IFrameBasedClock GameplayClock { get; private set; } = null!; + protected IFrameBasedClock FramedClock { get; private set; } = null!; + + private readonly BindableBool isPaused = new BindableBool(true); + + /// + /// The adjustable source clock used for gameplay. Should be used for seeks and clock control. + /// + private readonly DecoupleableInterpolatingFramedClock decoupledClock; /// /// Creates a new . @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both; - AdjustableSource = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; IsPaused.BindValueChanged(OnIsPausedChanged); } @@ -75,7 +75,7 @@ namespace osu.Game.Screens.Play { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - GameplayClock = CreateGameplayClock(AdjustableSource); + FramedClock = CreateGameplayClock(decoupledClock); dependencies.CacheAs(this); @@ -89,13 +89,13 @@ namespace osu.Game.Screens.Play { ensureSourceClockSet(); - if (!AdjustableSource.IsRunning) + if (!decoupledClock.IsRunning) { // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time // This accounts for the clock source potentially taking time to enter a completely stopped state - Seek(GameplayClock.CurrentTime); + Seek(FramedClock.CurrentTime); - AdjustableSource.Start(); + decoupledClock.Start(); } isPaused.Value = false; @@ -109,10 +109,10 @@ namespace osu.Game.Screens.Play { Logger.Log($"{nameof(GameplayClockContainer)} seeking to {time}"); - AdjustableSource.Seek(time); + decoupledClock.Seek(time); // Manually process to make sure the gameplay clock is correctly updated after a seek. - GameplayClock.ProcessFrame(); + FramedClock.ProcessFrame(); OnSeek?.Invoke(); } @@ -129,7 +129,7 @@ namespace osu.Game.Screens.Play public void Reset(bool startClock = false) { // Manually stop the source in order to not affect the IsPaused state. - AdjustableSource.Stop(); + decoupledClock.Stop(); if (!IsPaused.Value || startClock) Start(); @@ -142,10 +142,10 @@ namespace osu.Game.Screens.Play /// Changes the source clock. /// /// The new source. - protected void ChangeSource(IClock sourceClock) => AdjustableSource.ChangeSource(SourceClock = sourceClock); + protected void ChangeSource(IClock sourceClock) => decoupledClock.ChangeSource(SourceClock = sourceClock); /// - /// Ensures that the is set to , if it hasn't been given a source yet. + /// Ensures that the is set to , if it hasn't been given a source yet. /// This is usually done before a seek to avoid accidentally seeking only the adjustable source in decoupled mode, /// but not the actual source clock. /// That will pretty much only happen on the very first call of this method, as the source clock is passed in the constructor, @@ -153,38 +153,38 @@ namespace osu.Game.Screens.Play /// private void ensureSourceClockSet() { - if (AdjustableSource.Source == null) + if (decoupledClock.Source == null) ChangeSource(SourceClock); } protected override void Update() { if (!IsPaused.Value) - GameplayClock.ProcessFrame(); + FramedClock.ProcessFrame(); base.Update(); } /// - /// Invoked when the value of is changed to start or stop the clock. + /// Invoked when the value of is changed to start or stop the clock. /// /// Whether the clock should now be paused. protected virtual void OnIsPausedChanged(ValueChangedEvent isPaused) { if (isPaused.NewValue) - AdjustableSource.Stop(); + decoupledClock.Stop(); else - AdjustableSource.Start(); + decoupledClock.Start(); } /// - /// Creates the final which is exposed via DI to be used by gameplay components. + /// Creates the final which is exposed via DI to be used by gameplay components. /// /// /// Any intermediate clocks such as platform offsets should be applied here. /// /// The providing the source time. - /// The final . + /// The final . protected virtual IFrameBasedClock CreateGameplayClock(IFrameBasedClock source) => source; #region IAdjustableClock @@ -201,15 +201,15 @@ namespace osu.Game.Screens.Play double IAdjustableClock.Rate { - get => GameplayClock.Rate; + get => FramedClock.Rate; set => throw new NotSupportedException(); } - public double Rate => GameplayClock.Rate; + public double Rate => FramedClock.Rate; - public double CurrentTime => GameplayClock.CurrentTime; + public double CurrentTime => FramedClock.CurrentTime; - public bool IsRunning => GameplayClock.IsRunning; + public bool IsRunning => FramedClock.IsRunning; #endregion @@ -218,11 +218,11 @@ namespace osu.Game.Screens.Play // Handled via update. Don't process here to safeguard from external usages potentially processing frames additional times. } - public double ElapsedFrameTime => GameplayClock.ElapsedFrameTime; + public double ElapsedFrameTime => FramedClock.ElapsedFrameTime; - public double FramesPerSecond => GameplayClock.FramesPerSecond; + public double FramesPerSecond => FramedClock.FramesPerSecond; - public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo; + public FrameTimeInfo TimeInfo => FramedClock.TimeInfo; public double TrueGameplayRate { diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 0427792392..ea4f767109 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -35,8 +35,6 @@ namespace osu.Game.Screens.Play /// public const double MINIMUM_SKIP_TIME = 1000; - protected Track Track => (Track)SourceClock; - public readonly BindableNumber UserPlaybackRate = new BindableDouble(1) { Default = 1, @@ -133,7 +131,7 @@ namespace osu.Game.Screens.Play this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => { if (IsPaused.Value == isPaused.NewValue) - AdjustableSource.Stop(); + base.OnIsPausedChanged(isPaused); }); } else @@ -142,14 +140,14 @@ namespace osu.Game.Screens.Play else { if (isPaused.NewValue) - AdjustableSource.Stop(); + base.OnIsPausedChanged(isPaused); // If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations. pauseFreqAdjust.Value = isPaused.NewValue ? 0 : 1; // We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment. // Without doing this, an initial seek may be performed with the wrong offset. - GameplayClock.ProcessFrame(); + FramedClock.ProcessFrame(); } } @@ -178,12 +176,12 @@ namespace osu.Game.Screens.Play /// public void Skip() { - if (GameplayClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME) + if (FramedClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME) return; double skipTarget = skipTargetTime - MINIMUM_SKIP_TIME; - if (GameplayClock.CurrentTime < 0 && skipTarget > 6000) + if (FramedClock.CurrentTime < 0 && skipTarget > 6000) // double skip exception for storyboards with very long intros skipTarget = 0; @@ -218,8 +216,11 @@ namespace osu.Game.Screens.Play if (speedAdjustmentsApplied) return; - Track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - Track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + if (SourceClock is Track track) + { + track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + } nonGameplayAdjustments.Add(pauseFreqAdjust); nonGameplayAdjustments.Add(UserPlaybackRate); @@ -232,8 +233,11 @@ namespace osu.Game.Screens.Play if (!speedAdjustmentsApplied) return; - Track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - Track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + if (SourceClock is Track track) + { + track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + } nonGameplayAdjustments.Remove(pauseFreqAdjust); nonGameplayAdjustments.Remove(UserPlaybackRate); From 61a8873266d5345b22163a8aaaac26bdeacadd92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 20:09:07 +0900 Subject: [PATCH 144/532] Ensure `GameplayClockContainer`'s `FramedClock` is always non-null --- osu.Game/Screens/Play/GameplayClockContainer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index a5d6cbf2e1..ac846b45c4 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Play /// /// The final clock which is exposed to gameplay components. /// - protected IFrameBasedClock FramedClock { get; private set; } = null!; + protected IFrameBasedClock FramedClock { get; private set; } private readonly BindableBool isPaused = new BindableBool(true); @@ -69,6 +69,9 @@ namespace osu.Game.Screens.Play decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; IsPaused.BindValueChanged(OnIsPausedChanged); + + // this will be replaced during load, but non-null for tests which don't add this component to the hierarchy. + FramedClock = new FramedClock(); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) From 502e31dd37b3b8be70aeb55b0efde4e51e15244a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 15 Aug 2022 21:26:54 +0900 Subject: [PATCH 145/532] General refactoring --- .../Preprocessing/Colour/Data/MonoEncoding.cs | 1 - .../TaikoColourDifficultyPreprocessor.cs | 79 ++++++++----------- .../Colour/TaikoDifficultyHitObjectColour.cs | 9 +-- .../Preprocessing/TaikoDifficultyHitObject.cs | 43 +++++----- .../Difficulty/Skills/Colour.cs | 2 - .../Difficulty/Skills/Peaks.cs | 2 - 6 files changed, 57 insertions(+), 79 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs index 7eee8896ac..0e998696f9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs @@ -15,7 +15,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data { /// /// List of s that are encoded within this . - /// This is not declared as to avoid circular dependencies. /// public List EncodedData { get; private set; } = new List(); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index 517e240682..7b7fab26b1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -9,8 +9,7 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour { /// - /// Utility class to perform various encodings. This is separated out from the encoding classes to prevent circular - /// dependencies. + /// Utility class to perform various encodings. /// public class TaikoColourDifficultyPreprocessor { @@ -26,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour // Assign indexing and encoding data to all relevant objects. Only the first note of each encoding type is // assigned with the relevant encodings. - encodings.ForEach(coupledEncoding => + foreach (var coupledEncoding in encodings) { coupledEncoding.Payload[0].Payload[0].EncodedData[0].Colour.CoupledColourEncoding = coupledEncoding; @@ -48,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour monoEncoding.EncodedData[0].Colour.MonoEncoding = monoEncoding; } } - }); + } return colours; } @@ -58,35 +57,29 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// public static List EncodeMono(List data) { - List encoded = new List(); - - MonoEncoding? lastEncoded = null; + List encodings = new List(); + MonoEncoding? currentEncoding = null; for (int i = 0; i < data.Count; i++) { TaikoDifficultyHitObject taikoObject = (TaikoDifficultyHitObject)data[i]; + // This ignores all non-note objects, which may or may not be the desired behaviour TaikoDifficultyHitObject? previousObject = taikoObject.PreviousNote(0); - // If the colour changed or if this is the first object in the run, create a new mono encoding - if - ( - previousObject == null || // First object in the list - (taikoObject.BaseObject as Hit)?.Type != (previousObject.BaseObject as Hit)?.Type - ) + // If this is the first object in the list or the colour changed, create a new mono encoding + if (currentEncoding == null || (taikoObject.BaseObject as Hit)?.Type != (previousObject?.BaseObject as Hit)?.Type) { - lastEncoded = new MonoEncoding(); - lastEncoded.EncodedData.Add(taikoObject); - encoded.Add(lastEncoded); + currentEncoding = new MonoEncoding(); + encodings.Add(currentEncoding); continue; } - // If we're here, we're in the same encoding as the previous object, thus lastEncoded is not null. // Add the current object to the encoded payload. - lastEncoded!.EncodedData.Add(taikoObject); + currentEncoding.EncodedData.Add(taikoObject); } - return encoded; + return encodings; } /// @@ -94,27 +87,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// public static List EncodeColour(List data) { - List encoded = new List(); - ColourEncoding? lastEncoded = null; + List encodings = new List(); + ColourEncoding? currentEncoding = null; for (int i = 0; i < data.Count; i++) { - // Starts a new ColourEncoding if the previous MonoEncoding has a different mono length, or if this is - // the first MonoEncoding in the list. - if (lastEncoded == null || data[i].RunLength != data[i - 1].RunLength) + // Start a new ColourEncoding if the previous MonoEncoding has a different mono length, or if this is the first MonoEncoding in the list. + if (currentEncoding == null || data[i].RunLength != data[i - 1].RunLength) { - lastEncoded = new ColourEncoding(); - lastEncoded.Payload.Add(data[i]); - encoded.Add(lastEncoded); + currentEncoding = new ColourEncoding(); + encodings.Add(currentEncoding); continue; } - // If we're here, we're in the same encoding as the previous object. Add the current MonoEncoding to the - // encoded payload. - lastEncoded.Payload.Add(data[i]); + // Add the current MonoEncoding to the encoded payload. + currentEncoding.Payload.Add(data[i]); } - return encoded; + return encodings; } /// @@ -122,16 +112,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// public static List EncodeCoupledColour(List data) { - List encoded = new List(); - CoupledColourEncoding? lastEncoded = null; + List encodings = new List(); + CoupledColourEncoding? currentEncoding = null; for (int i = 0; i < data.Count; i++) { - // Starts a new CoupledColourEncoding. ColourEncodings that should be grouped together will be handled - // later within this loop. - lastEncoded = new CoupledColourEncoding + // Start a new CoupledColourEncoding. ColourEncodings that should be grouped together will be handled later within this loop. + currentEncoding = new CoupledColourEncoding { - Previous = lastEncoded + Previous = currentEncoding }; // Determine if future ColourEncodings should be grouped. @@ -140,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour if (!isCoupled) { // If not, add the current ColourEncoding to the encoded payload and continue. - lastEncoded.Payload.Add(data[i]); + currentEncoding.Payload.Add(data[i]); } else { @@ -148,27 +137,27 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour // subsequent ColourEncodings should be grouped by increasing i and doing the appropriate isCoupled check. while (isCoupled) { - lastEncoded.Payload.Add(data[i]); + currentEncoding.Payload.Add(data[i]); i++; isCoupled = i < data.Count - 2 && data[i].IsRepetitionOf(data[i + 2]); } // Skip over viewed data and add the rest to the payload - lastEncoded.Payload.Add(data[i]); - lastEncoded.Payload.Add(data[i + 1]); + currentEncoding.Payload.Add(data[i]); + currentEncoding.Payload.Add(data[i + 1]); i++; } - encoded.Add(lastEncoded); + encodings.Add(currentEncoding); } // Final pass to find repetition intervals - for (int i = 0; i < encoded.Count; i++) + for (int i = 0; i < encodings.Count; i++) { - encoded[i].FindRepetitionInterval(); + encodings[i].FindRepetitionInterval(); } - return encoded; + return encodings; } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs index 6a6b427393..41080eeb33 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs @@ -11,20 +11,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour public class TaikoDifficultyHitObjectColour { /// - /// encoding that encodes this note, only present if this is the first note within a - /// + /// The that encodes this note, only present if this is the first note within a /// public MonoEncoding? MonoEncoding; /// - /// encoding that encodes this note, only present if this is the first note within - /// a + /// The that encodes this note, only present if this is the first note within a /// public ColourEncoding? ColourEncoding; /// - /// encoding that encodes this note, only present if this is the first note - /// within a + /// The that encodes this note, only present if this is the first note within a /// public CoupledColourEncoding? CoupledColourEncoding; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index fd9a225f6a..e7a8abfd38 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -45,9 +45,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used /// by other skills in the future. - /// This need to be writeable by TaikoDifficultyHitObjectColour so that it can assign potentially reused instances /// - public TaikoDifficultyHitObjectColour Colour; + public readonly TaikoDifficultyHitObjectColour Colour; /// /// Creates a new difficulty hit object. @@ -59,7 +58,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing /// The list of all s in the current beatmap. /// The list of centre (don) s in the current beatmap. /// The list of rim (kat) s in the current beatmap. - /// The list of s that is a hit (i.e. not a slider or spinner) in the current beatmap. + /// The list of s that is a hit (i.e. not a drumroll or swell) in the current beatmap. /// The position of this in the list. public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, List objects, @@ -68,33 +67,31 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing List noteObjects, int index) : base(hitObject, lastObject, clockRate, objects, index) { - // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor - Colour = new TaikoDifficultyHitObjectColour(); - - var currentHit = hitObject as Hit; noteDifficultyHitObjects = noteObjects; + // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor + Colour = new TaikoDifficultyHitObjectColour(); Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate); - HitType? hitType = currentHit?.Type; - if (hitType == HitType.Centre) + switch ((hitObject as Hit)?.Type) { - MonoIndex = centreHitObjects.Count; - centreHitObjects.Add(this); - monoDifficultyHitObjects = centreHitObjects; - } - else if (hitType == HitType.Rim) - { - MonoIndex = rimHitObjects.Count; - rimHitObjects.Add(this); - monoDifficultyHitObjects = rimHitObjects; - } + case HitType.Centre: + MonoIndex = centreHitObjects.Count; + centreHitObjects.Add(this); + monoDifficultyHitObjects = centreHitObjects; + break; - // Need to be done after HitType is set. - if (hitType == null) return; + case HitType.Rim: + MonoIndex = rimHitObjects.Count; + rimHitObjects.Add(this); + monoDifficultyHitObjects = rimHitObjects; + break; - NoteIndex = noteObjects.Count; - noteObjects.Add(this); + default: + NoteIndex = noteObjects.Count; + noteObjects.Add(this); + break; + } } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 386135ea4d..dac0beadda 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs index 3e3bc543e1..ec8e754c5c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; From 94c6beeaf7c465ff22b229863388701f94b71314 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 15 Aug 2022 21:30:40 +0900 Subject: [PATCH 146/532] Use ctor in a place that looks visually weird I read through this thinking "why doesn't Previous get assigned to currentEncoding here? But it's because the initializer runs right after the ctor and before the "method" returns. So really there's 3 operations running on one line here - ctor, init, and assignment. --- .../Preprocessing/Colour/Data/ColourEncoding.cs | 2 +- .../Preprocessing/Colour/Data/CoupledColourEncoding.cs | 9 +++++++-- .../Colour/TaikoColourDifficultyPreprocessor.cs | 5 +---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs index 04066e7539..cd39a3d232 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// /// s that are grouped together within this . /// - public List Payload { get; private set; } = new List(); + public readonly List Payload = new List(); /// /// The parent that contains this diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs index 9d204225fc..1b831eedd8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs @@ -20,12 +20,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// /// The s that are grouped together within this . /// - public List Payload = new List(); + public readonly List Payload = new List(); /// /// The previous . This is used to determine the repetition interval. /// - public CoupledColourEncoding? Previous = null; + public readonly CoupledColourEncoding? Previous; /// /// How many between the current and previous identical . @@ -33,6 +33,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// public int RepetitionInterval { get; private set; } = max_repetition_interval + 1; + public CoupledColourEncoding(CoupledColourEncoding? previous) + { + Previous = previous; + } + /// /// Returns true if other is considered a repetition of this encoding. This is true if other's first two payloads /// have identical mono lengths. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index 7b7fab26b1..2d69f5fe35 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -118,10 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour for (int i = 0; i < data.Count; i++) { // Start a new CoupledColourEncoding. ColourEncodings that should be grouped together will be handled later within this loop. - currentEncoding = new CoupledColourEncoding - { - Previous = currentEncoding - }; + currentEncoding = new CoupledColourEncoding(currentEncoding); // Determine if future ColourEncodings should be grouped. bool isCoupled = i < data.Count - 2 && data[i].IsRepetitionOf(data[i + 2]); From 21d29980329a4ff4b114abb3cf4430ee3e7eff8d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 15 Aug 2022 21:35:34 +0900 Subject: [PATCH 147/532] Privatise internals of TaikoColourDifficultyPreprocessor --- .../TaikoColourDifficultyPreprocessor.cs | 34 +++++++++---------- .../TaikoDifficultyPreprocessor.cs | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index 2d69f5fe35..2b047a4336 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// /// Utility class to perform various encodings. /// - public class TaikoColourDifficultyPreprocessor + public static class TaikoColourDifficultyPreprocessor { /// /// Processes and encodes a list of s into a list of s, @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour public static List ProcessAndAssign(List hitObjects) { List colours = new List(); - List encodings = Encode(hitObjects); + List encodings = encode(hitObjects); // Assign indexing and encoding data to all relevant objects. Only the first note of each encoding type is // assigned with the relevant encodings. @@ -52,10 +52,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour return colours; } + /// + /// Encodes a list of s into a list of s. + /// + private static List encode(List data) + { + List firstPass = encodeMono(data); + List secondPass = encodeColour(firstPass); + List thirdPass = encodeCoupledColour(secondPass); + + return thirdPass; + } + /// /// Encodes a list of s into a list of s. /// - public static List EncodeMono(List data) + private static List encodeMono(List data) { List encodings = new List(); MonoEncoding? currentEncoding = null; @@ -85,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// /// Encodes a list of s into a list of s. /// - public static List EncodeColour(List data) + private static List encodeColour(List data) { List encodings = new List(); ColourEncoding? currentEncoding = null; @@ -110,7 +122,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// /// Encodes a list of s into a list of s. /// - public static List EncodeCoupledColour(List data) + private static List encodeCoupledColour(List data) { List encodings = new List(); CoupledColourEncoding? currentEncoding = null; @@ -156,17 +168,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour return encodings; } - - /// - /// Encodes a list of s into a list of s. - /// - public static List Encode(List data) - { - List firstPass = EncodeMono(data); - List secondPass = EncodeColour(firstPass); - List thirdPass = EncodeCoupledColour(secondPass); - - return thirdPass; - } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs index c5ee8de809..2223c8e2d1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { - public class TaikoDifficultyPreprocessor + public static class TaikoDifficultyPreprocessor { /// /// Does preprocessing on a list of s. From 78283ce3c5993133c227c1113e8891e0b03fb4ab Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 15 Aug 2022 21:38:40 +0900 Subject: [PATCH 148/532] Remove TaikoDifficultyPreprocessor --- .../TaikoColourDifficultyPreprocessor.cs | 5 +---- .../TaikoDifficultyPreprocessor.cs | 21 ------------------- .../Difficulty/TaikoDifficultyCalculator.cs | 5 ++++- 3 files changed, 5 insertions(+), 26 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index 2b047a4336..38d51aef91 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -18,9 +18,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// assigning the appropriate s to each , /// and pre-evaluating colour difficulty of each . /// - public static List ProcessAndAssign(List hitObjects) + public static void ProcessAndAssign(List hitObjects) { - List colours = new List(); List encodings = encode(hitObjects); // Assign indexing and encoding data to all relevant objects. Only the first note of each encoding type is @@ -48,8 +47,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour } } } - - return colours; } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs deleted file mode 100644 index 2223c8e2d1..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyPreprocessor.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing -{ - public static class TaikoDifficultyPreprocessor - { - /// - /// Does preprocessing on a list of s. - /// - public static List Process(List difficultyHitObjects) - { - TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); - return difficultyHitObjects; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index ceaa3c56b5..ea2f04a3d9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; @@ -63,7 +64,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty ); } - return TaikoDifficultyPreprocessor.Process(difficultyHitObjects); + TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); + + return difficultyHitObjects; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) From 4d4ee05981ca32f09d6a4c1996b8a765705821f5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 15 Aug 2022 21:48:04 +0900 Subject: [PATCH 149/532] Whoops I meant to remove these --- .../Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index 38d51aef91..0d1d3cd7a9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -81,7 +81,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour { currentEncoding = new MonoEncoding(); encodings.Add(currentEncoding); - continue; } // Add the current object to the encoded payload. @@ -106,7 +105,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour { currentEncoding = new ColourEncoding(); encodings.Add(currentEncoding); - continue; } // Add the current MonoEncoding to the encoded payload. From c03e47317a30cd9ac914f481cbb71677586eac72 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 15 Aug 2022 21:54:23 +0900 Subject: [PATCH 150/532] Fix notes not being added to list --- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index e7a8abfd38..4aaee50c18 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -86,11 +86,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing rimHitObjects.Add(this); monoDifficultyHitObjects = rimHitObjects; break; + } - default: - NoteIndex = noteObjects.Count; - noteObjects.Add(this); - break; + if (hitObject is Hit) + { + NoteIndex = noteObjects.Count; + noteObjects.Add(this); } } From 8e0049c00548ef64c7faee7ed480370c274efe95 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 15 Aug 2022 21:57:35 +0900 Subject: [PATCH 151/532] Add back null check --- .../Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index 0d1d3cd7a9..81ba219bc0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour TaikoDifficultyHitObject? previousObject = taikoObject.PreviousNote(0); // If this is the first object in the list or the colour changed, create a new mono encoding - if (currentEncoding == null || (taikoObject.BaseObject as Hit)?.Type != (previousObject?.BaseObject as Hit)?.Type) + if (currentEncoding == null || previousObject == null || (taikoObject.BaseObject as Hit)?.Type != (previousObject.BaseObject as Hit)?.Type) { currentEncoding = new MonoEncoding(); encodings.Add(currentEncoding); From 797a8da996cd16aa5e0543a9b4482a4ba2483df1 Mon Sep 17 00:00:00 2001 From: its5Q Date: Tue, 16 Aug 2022 01:14:16 +1000 Subject: [PATCH 152/532] Replace osu-web strings with new strings and merge to single file --- .../Localisation/EditorSetupColoursStrings.cs | 24 --- .../Localisation/EditorSetupDesignStrings.cs | 84 -------- .../EditorSetupDifficultyStrings.cs | 34 ---- .../EditorSetupMetadataStrings.cs | 39 ---- .../EditorSetupResourcesStrings.cs | 44 ----- .../Localisation/EditorSetupRulesetStrings.cs | 19 -- osu.Game/Localisation/EditorSetupStrings.cs | 180 ++++++++++++++++++ osu.Game/Screens/Edit/Setup/ColoursSection.cs | 8 +- osu.Game/Screens/Edit/Setup/DesignSection.cs | 28 +-- .../Screens/Edit/Setup/DifficultySection.cs | 10 +- .../Screens/Edit/Setup/MetadataSection.cs | 14 +- .../Screens/Edit/Setup/ResourcesSection.cs | 12 +- .../Screens/Edit/Setup/RulesetSetupSection.cs | 2 +- 13 files changed, 216 insertions(+), 282 deletions(-) delete mode 100644 osu.Game/Localisation/EditorSetupColoursStrings.cs delete mode 100644 osu.Game/Localisation/EditorSetupDesignStrings.cs delete mode 100644 osu.Game/Localisation/EditorSetupDifficultyStrings.cs delete mode 100644 osu.Game/Localisation/EditorSetupMetadataStrings.cs delete mode 100644 osu.Game/Localisation/EditorSetupResourcesStrings.cs delete mode 100644 osu.Game/Localisation/EditorSetupRulesetStrings.cs diff --git a/osu.Game/Localisation/EditorSetupColoursStrings.cs b/osu.Game/Localisation/EditorSetupColoursStrings.cs deleted file mode 100644 index e08240a7d2..0000000000 --- a/osu.Game/Localisation/EditorSetupColoursStrings.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Localisation; - -namespace osu.Game.Localisation -{ - public static class EditorSetupColoursStrings - { - private const string prefix = @"osu.Game.Resources.Localisation.EditorSetupColours"; - - /// - /// "Colours" - /// - public static LocalisableString Colours => new TranslatableString(getKey(@"colours"), @"Colours"); - - /// - /// "Hitcircle / Slider Combos" - /// - public static LocalisableString HitcircleSliderCombos => new TranslatableString(getKey(@"hitcircle_slider_combos"), @"Hitcircle / Slider Combos"); - - private static string getKey(string key) => $@"{prefix}:{key}"; - } -} diff --git a/osu.Game/Localisation/EditorSetupDesignStrings.cs b/osu.Game/Localisation/EditorSetupDesignStrings.cs deleted file mode 100644 index 0a5e383b76..0000000000 --- a/osu.Game/Localisation/EditorSetupDesignStrings.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Localisation; - -namespace osu.Game.Localisation -{ - public static class EditorSetupDesignStrings - { - private const string prefix = @"osu.Game.Resources.Localisation.EditorSetupDesign"; - - /// - /// "Design" - /// - public static LocalisableString Design => new TranslatableString(getKey(@"design"), @"Design"); - - /// - /// "Enable countdown" - /// - public static LocalisableString EnableCountdown => new TranslatableString(getKey(@"enable_countdown"), @"Enable countdown"); - - /// - /// "If enabled, an "Are you ready? 3, 2, 1, GO!" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so." - /// - public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"), @"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."); - - /// - /// "Countdown speed" - /// - public static LocalisableString CountdownSpeed => new TranslatableString(getKey(@"countdown_speed"), @"Countdown speed"); - - /// - /// "If the countdown sounds off-time, use this to make it appear one or more beats early." - /// - public static LocalisableString CountdownOffsetDescription => new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early."); - - /// - /// "Countdown offset" - /// - public static LocalisableString CountdownOffset => new TranslatableString(getKey(@"countdown_offset"), @"Countdown offset"); - - /// - /// "Widescreen support" - /// - public static LocalisableString WidescreenSupport => new TranslatableString(getKey(@"widescreen_support"), @"Widescreen support"); - - /// - /// "Allows storyboards to use the full screen space, rather than be confined to a 4:3 area." - /// - public static LocalisableString WidescreenSupportDescription => new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."); - - /// - /// "Epilepsy warning" - /// - public static LocalisableString EpilepsyWarning => new TranslatableString(getKey(@"epilepsy_warning"), @"Epilepsy warning"); - - /// - /// "Recommended if the storyboard or video contain scenes with rapidly flashing colours." - /// - public static LocalisableString EpilepsyWarningDescription => new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours."); - - /// - /// "Letterbox during breaks" - /// - public static LocalisableString LetterboxDuringBreaks => new TranslatableString(getKey(@"letterbox_during_breaks"), @"Letterbox during breaks"); - - /// - /// "Adds horizontal letterboxing to give a cinematic look during breaks." - /// - public static LocalisableString LetterboxDuringBreaksDescription => new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks."); - - /// - /// "Samples match playback rate" - /// - public static LocalisableString SamplesMatchPlaybackRate => new TranslatableString(getKey(@"samples_match_playback_rate"), @"Samples match playback rate"); - - /// - /// "When enabled, all samples will speed up or slow down when rate-changing mods are enabled." - /// - public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"), @"When enabled, all samples will speed up or slow down when rate-changing mods are enabled."); - - private static string getKey(string key) => $@"{prefix}:{key}"; - } -} diff --git a/osu.Game/Localisation/EditorSetupDifficultyStrings.cs b/osu.Game/Localisation/EditorSetupDifficultyStrings.cs deleted file mode 100644 index cdb7837a64..0000000000 --- a/osu.Game/Localisation/EditorSetupDifficultyStrings.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Localisation; - -namespace osu.Game.Localisation -{ - public static class EditorSetupDifficultyStrings - { - private const string prefix = @"osu.Game.Resources.Localisation.EditorSetupDifficulty"; - - /// - /// "The size of all hit objects" - /// - public static LocalisableString CircleSizeDescription => new TranslatableString(getKey(@"circle_size_description"), @"The size of all hit objects"); - - /// - /// "The rate of passive health drain throughout playable time" - /// - public static LocalisableString DrainRateDescription => new TranslatableString(getKey(@"drain_rate_description"), @"The rate of passive health drain throughout playable time"); - - /// - /// "The speed at which objects are presented to the player" - /// - public static LocalisableString ApproachRateDescription => new TranslatableString(getKey(@"approach_rate_description"), @"The speed at which objects are presented to the player"); - - /// - /// "The harshness of hit windows and difficulty of special objects (ie. spinners)" - /// - public static LocalisableString OverallDifficultyDescription => new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)"); - - private static string getKey(string key) => $@"{prefix}:{key}"; - } -} diff --git a/osu.Game/Localisation/EditorSetupMetadataStrings.cs b/osu.Game/Localisation/EditorSetupMetadataStrings.cs deleted file mode 100644 index 576fa68643..0000000000 --- a/osu.Game/Localisation/EditorSetupMetadataStrings.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Localisation; - -namespace osu.Game.Localisation -{ - public static class EditorSetupMetadataStrings - { - private const string prefix = @"osu.Game.Resources.Localisation.EditorSetupMetadata"; - - /// - /// "Metadata" - /// - public static LocalisableString Metadata => new TranslatableString(getKey(@"metadata"), @"Metadata"); - - /// - /// "Romanised Artist" - /// - public static LocalisableString RomanisedArtist => new TranslatableString(getKey(@"romanised_artist"), @"Romanised Artist"); - - /// - /// "Romanised Title" - /// - public static LocalisableString RomanisedTitle => new TranslatableString(getKey(@"romanised_title"), @"Romanised Title"); - - /// - /// "Creator" - /// - public static LocalisableString Creator => new TranslatableString(getKey(@"creator"), @"Creator"); - - /// - /// "Difficulty Name" - /// - public static LocalisableString DifficultyName => new TranslatableString(getKey(@"difficulty_name"), @"Difficulty Name"); - - private static string getKey(string key) => $@"{prefix}:{key}"; - } -} diff --git a/osu.Game/Localisation/EditorSetupResourcesStrings.cs b/osu.Game/Localisation/EditorSetupResourcesStrings.cs deleted file mode 100644 index 493beae7fe..0000000000 --- a/osu.Game/Localisation/EditorSetupResourcesStrings.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Localisation; - -namespace osu.Game.Localisation -{ - public static class EditorSetupResourcesStrings - { - private const string prefix = @"osu.Game.Resources.Localisation.EditorSetupResources"; - - /// - /// "Resources" - /// - public static LocalisableString Resources => new TranslatableString(getKey(@"resources"), @"Resources"); - - /// - /// "Audio Track" - /// - public static LocalisableString AudioTrack => new TranslatableString(getKey(@"audio_track"), @"Audio Track"); - - /// - /// "Click to select a track" - /// - public static LocalisableString ClickToSelectTrack => new TranslatableString(getKey(@"click_to_select_track"), @"Click to select a track"); - - /// - /// "Click to replace the track" - /// - public static LocalisableString ClickToReplaceTrack => new TranslatableString(getKey(@"click_to_replace_track"), @"Click to replace the track"); - - /// - /// "Click to select a background image" - /// - public static LocalisableString ClickToSelectBackground => new TranslatableString(getKey(@"click_to_select_background"), @"Click to select a background image"); - - /// - /// "Click to replace the background image" - /// - public static LocalisableString ClickToReplaceBackground => new TranslatableString(getKey(@"click_to_replace_background"), @"Click to replace the background image"); - - private static string getKey(string key) => $@"{prefix}:{key}"; - } -} diff --git a/osu.Game/Localisation/EditorSetupRulesetStrings.cs b/osu.Game/Localisation/EditorSetupRulesetStrings.cs deleted file mode 100644 index a786b679a3..0000000000 --- a/osu.Game/Localisation/EditorSetupRulesetStrings.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Localisation; - -namespace osu.Game.Localisation -{ - public static class EditorSetupRulesetStrings - { - private const string prefix = @"osu.Game.Resources.Localisation.EditorSetupRuleset"; - - /// - /// "Ruleset ({0})" - /// - public static LocalisableString Ruleset(string arg0) => new TranslatableString(getKey(@"ruleset"), @"Ruleset ({0})", arg0); - - private static string getKey(string key) => $@"{prefix}:{key}"; - } -} diff --git a/osu.Game/Localisation/EditorSetupStrings.cs b/osu.Game/Localisation/EditorSetupStrings.cs index 97ebd40b6f..9512f3ff14 100644 --- a/osu.Game/Localisation/EditorSetupStrings.cs +++ b/osu.Game/Localisation/EditorSetupStrings.cs @@ -19,6 +19,186 @@ namespace osu.Game.Localisation /// public static LocalisableString BeatmapSetupDescription => new TranslatableString(getKey(@"beatmap_setup_description"), @"change general settings of your beatmap"); + /// + /// "Colours" + /// + public static LocalisableString ColoursHeader => new TranslatableString(getKey(@"colours_header"), @"Colours"); + + /// + /// "Hitcircle / Slider Combos" + /// + public static LocalisableString HitcircleSliderCombos => new TranslatableString(getKey(@"hitcircle_slider_combos"), @"Hitcircle / Slider Combos"); + + /// + /// "Design" + /// + public static LocalisableString DesignHeader => new TranslatableString(getKey(@"design_header"), @"Design"); + + /// + /// "Enable countdown" + /// + public static LocalisableString EnableCountdown => new TranslatableString(getKey(@"enable_countdown"), @"Enable countdown"); + + /// + /// "If enabled, an "Are you ready? 3, 2, 1, GO!" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so." + /// + public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"), @"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."); + + /// + /// "Countdown speed" + /// + public static LocalisableString CountdownSpeed => new TranslatableString(getKey(@"countdown_speed"), @"Countdown speed"); + + /// + /// "If the countdown sounds off-time, use this to make it appear one or more beats early." + /// + public static LocalisableString CountdownOffsetDescription => new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early."); + + /// + /// "Countdown offset" + /// + public static LocalisableString CountdownOffset => new TranslatableString(getKey(@"countdown_offset"), @"Countdown offset"); + + /// + /// "Widescreen support" + /// + public static LocalisableString WidescreenSupport => new TranslatableString(getKey(@"widescreen_support"), @"Widescreen support"); + + /// + /// "Allows storyboards to use the full screen space, rather than be confined to a 4:3 area." + /// + public static LocalisableString WidescreenSupportDescription => new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."); + + /// + /// "Epilepsy warning" + /// + public static LocalisableString EpilepsyWarning => new TranslatableString(getKey(@"epilepsy_warning"), @"Epilepsy warning"); + + /// + /// "Recommended if the storyboard or video contain scenes with rapidly flashing colours." + /// + public static LocalisableString EpilepsyWarningDescription => new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours."); + + /// + /// "Letterbox during breaks" + /// + public static LocalisableString LetterboxDuringBreaks => new TranslatableString(getKey(@"letterbox_during_breaks"), @"Letterbox during breaks"); + + /// + /// "Adds horizontal letterboxing to give a cinematic look during breaks." + /// + public static LocalisableString LetterboxDuringBreaksDescription => new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks."); + + /// + /// "Samples match playback rate" + /// + public static LocalisableString SamplesMatchPlaybackRate => new TranslatableString(getKey(@"samples_match_playback_rate"), @"Samples match playback rate"); + + /// + /// "When enabled, all samples will speed up or slow down when rate-changing mods are enabled." + /// + public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"), @"When enabled, all samples will speed up or slow down when rate-changing mods are enabled."); + + /// + /// "The size of all hit objects" + /// + public static LocalisableString CircleSizeDescription => new TranslatableString(getKey(@"circle_size_description"), @"The size of all hit objects"); + + /// + /// "The rate of passive health drain throughout playable time" + /// + public static LocalisableString DrainRateDescription => new TranslatableString(getKey(@"drain_rate_description"), @"The rate of passive health drain throughout playable time"); + + /// + /// "The speed at which objects are presented to the player" + /// + public static LocalisableString ApproachRateDescription => new TranslatableString(getKey(@"approach_rate_description"), @"The speed at which objects are presented to the player"); + + /// + /// "The harshness of hit windows and difficulty of special objects (ie. spinners)" + /// + public static LocalisableString OverallDifficultyDescription => new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)"); + + /// + /// "Metadata" + /// + public static LocalisableString MetadataHeader => new TranslatableString(getKey(@"metadata_header"), @"Metadata"); + + /// + /// "Romanised Artist" + /// + public static LocalisableString RomanisedArtist => new TranslatableString(getKey(@"romanised_artist"), @"Romanised Artist"); + + /// + /// "Romanised Title" + /// + public static LocalisableString RomanisedTitle => new TranslatableString(getKey(@"romanised_title"), @"Romanised Title"); + + /// + /// "Creator" + /// + public static LocalisableString Creator => new TranslatableString(getKey(@"creator"), @"Creator"); + + /// + /// "Difficulty Name" + /// + public static LocalisableString DifficultyName => new TranslatableString(getKey(@"difficulty_name"), @"Difficulty Name"); + + /// + /// "Resources" + /// + public static LocalisableString ResourcesHeader => new TranslatableString(getKey(@"resources_header"), @"Resources"); + + /// + /// "Audio Track" + /// + public static LocalisableString AudioTrack => new TranslatableString(getKey(@"audio_track"), @"Audio Track"); + + /// + /// "Click to select a track" + /// + public static LocalisableString ClickToSelectTrack => new TranslatableString(getKey(@"click_to_select_track"), @"Click to select a track"); + + /// + /// "Click to replace the track" + /// + public static LocalisableString ClickToReplaceTrack => new TranslatableString(getKey(@"click_to_replace_track"), @"Click to replace the track"); + + /// + /// "Click to select a background image" + /// + public static LocalisableString ClickToSelectBackground => new TranslatableString(getKey(@"click_to_select_background"), @"Click to select a background image"); + + /// + /// "Click to replace the background image" + /// + public static LocalisableString ClickToReplaceBackground => new TranslatableString(getKey(@"click_to_replace_background"), @"Click to replace the background image"); + + /// + /// "Ruleset ({0})" + /// + public static LocalisableString RulesetHeader(string arg0) => new TranslatableString(getKey(@"ruleset"), @"Ruleset ({0})", arg0); + + /// + /// "Combo" + /// + public static LocalisableString ComboColourPrefix => new TranslatableString(getKey(@"combo_colour_prefix"), @"Combo"); + + /// + /// "Artist" + /// + public static LocalisableString Artist => new TranslatableString(getKey(@"artist"), @"Artist"); + + /// + /// "Title" + /// + public static LocalisableString Title => new TranslatableString(getKey(@"title"), @"Title"); + + /// + /// "Difficulty" + /// + public static LocalisableString DifficultyHeader => new TranslatableString(getKey(@"difficulty_header"), @"Difficulty"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Edit/Setup/ColoursSection.cs b/osu.Game/Screens/Edit/Setup/ColoursSection.cs index 5792613aa0..607086fdba 100644 --- a/osu.Game/Screens/Edit/Setup/ColoursSection.cs +++ b/osu.Game/Screens/Edit/Setup/ColoursSection.cs @@ -8,13 +8,12 @@ using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; -using osu.Game.Resources.Localisation.Web; namespace osu.Game.Screens.Edit.Setup { internal class ColoursSection : SetupSection { - public override LocalisableString Title => EditorSetupColoursStrings.Colours; + public override LocalisableString Title => EditorSetupStrings.ColoursHeader; private LabelledColourPalette comboColours; @@ -25,10 +24,9 @@ namespace osu.Game.Screens.Edit.Setup { comboColours = new LabelledColourPalette { - Label = EditorSetupColoursStrings.HitcircleSliderCombos, + Label = EditorSetupStrings.HitcircleSliderCombos, FixedLabelWidth = LABEL_WIDTH, - ColourNamePrefix = MatchesStrings.MatchScoreStatsCombo - } + ColourNamePrefix = EditorSetupStrings.ComboColourPrefix } }; if (Beatmap.BeatmapSkin != null) diff --git a/osu.Game/Screens/Edit/Setup/DesignSection.cs b/osu.Game/Screens/Edit/Setup/DesignSection.cs index 6214a17529..03d4b061d1 100644 --- a/osu.Game/Screens/Edit/Setup/DesignSection.cs +++ b/osu.Game/Screens/Edit/Setup/DesignSection.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Edit.Setup private LabelledSwitchButton letterboxDuringBreaks; private LabelledSwitchButton samplesMatchPlaybackRate; - public override LocalisableString Title => EditorSetupDesignStrings.Design; + public override LocalisableString Title => EditorSetupStrings.DesignHeader; [BackgroundDependencyLoader] private void load() @@ -39,9 +39,9 @@ namespace osu.Game.Screens.Edit.Setup { EnableCountdown = new LabelledSwitchButton { - Label = EditorSetupDesignStrings.EnableCountdown, + Label = EditorSetupStrings.EnableCountdown, Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None }, - Description = EditorSetupDesignStrings.CountdownDescription + Description = EditorSetupStrings.CountdownDescription }, CountdownSettings = new FillFlowContainer { @@ -53,41 +53,41 @@ namespace osu.Game.Screens.Edit.Setup { CountdownSpeed = new LabelledEnumDropdown { - Label = EditorSetupDesignStrings.CountdownSpeed, + Label = EditorSetupStrings.CountdownSpeed, Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None ? Beatmap.BeatmapInfo.Countdown : CountdownType.Normal }, Items = Enum.GetValues(typeof(CountdownType)).Cast().Where(type => type != CountdownType.None) }, CountdownOffset = new LabelledNumberBox { - Label = EditorSetupDesignStrings.CountdownOffset, + Label = EditorSetupStrings.CountdownOffset, Current = { Value = Beatmap.BeatmapInfo.CountdownOffset.ToString() }, - Description = EditorSetupDesignStrings.CountdownOffsetDescription, + Description = EditorSetupStrings.CountdownOffsetDescription, } } }, Empty(), widescreenSupport = new LabelledSwitchButton { - Label = EditorSetupDesignStrings.WidescreenSupport, - Description = EditorSetupDesignStrings.WidescreenSupportDescription, + Label = EditorSetupStrings.WidescreenSupport, + Description = EditorSetupStrings.WidescreenSupportDescription, Current = { Value = Beatmap.BeatmapInfo.WidescreenStoryboard } }, epilepsyWarning = new LabelledSwitchButton { - Label = EditorSetupDesignStrings.EpilepsyWarning, - Description = EditorSetupDesignStrings.EpilepsyWarningDescription, + Label = EditorSetupStrings.EpilepsyWarning, + Description = EditorSetupStrings.EpilepsyWarningDescription, Current = { Value = Beatmap.BeatmapInfo.EpilepsyWarning } }, letterboxDuringBreaks = new LabelledSwitchButton { - Label = EditorSetupDesignStrings.LetterboxDuringBreaks, - Description = EditorSetupDesignStrings.LetterboxDuringBreaksDescription, + Label = EditorSetupStrings.LetterboxDuringBreaks, + Description = EditorSetupStrings.LetterboxDuringBreaksDescription, Current = { Value = Beatmap.BeatmapInfo.LetterboxInBreaks } }, samplesMatchPlaybackRate = new LabelledSwitchButton { - Label = EditorSetupDesignStrings.SamplesMatchPlaybackRate, - Description = EditorSetupDesignStrings.SamplesMatchPlaybackRateDescription, + Label = EditorSetupStrings.SamplesMatchPlaybackRate, + Description = EditorSetupStrings.SamplesMatchPlaybackRateDescription, Current = { Value = Beatmap.BeatmapInfo.SamplesMatchPlaybackRate } } }; diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index d90997653c..93e1c1ee1b 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Edit.Setup private LabelledSliderBar approachRateSlider; private LabelledSliderBar overallDifficultySlider; - public override LocalisableString Title => BeatmapDiscussionsStrings.OwnerEditorVersion; + public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; [BackgroundDependencyLoader] private void load() @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = BeatmapsetsStrings.ShowStatsCs, FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupDifficultyStrings.CircleSizeDescription, + Description = EditorSetupStrings.CircleSizeDescription, Current = new BindableFloat(Beatmap.Difficulty.CircleSize) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = BeatmapsetsStrings.ShowStatsDrain, FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupDifficultyStrings.DrainRateDescription, + Description = EditorSetupStrings.DrainRateDescription, Current = new BindableFloat(Beatmap.Difficulty.DrainRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = BeatmapsetsStrings.ShowStatsAr, FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupDifficultyStrings.ApproachRateDescription, + Description = EditorSetupStrings.ApproachRateDescription, Current = new BindableFloat(Beatmap.Difficulty.ApproachRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = BeatmapsetsStrings.ShowStatsAccuracy, FixedLabelWidth = LABEL_WIDTH, - Description = EditorSetupDifficultyStrings.OverallDifficultyDescription, + Description = EditorSetupStrings.OverallDifficultyDescription, Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 1af749160b..807e68dad0 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Edit.Setup private LabelledTextBox sourceTextBox; private LabelledTextBox tagsTextBox; - public override LocalisableString Title => EditorSetupMetadataStrings.Metadata; + public override LocalisableString Title => EditorSetupStrings.MetadataHeader; [BackgroundDependencyLoader] private void load() @@ -36,22 +36,22 @@ namespace osu.Game.Screens.Edit.Setup Children = new[] { - ArtistTextBox = createTextBox(ArtistStrings.TracksIndexFormArtist, + ArtistTextBox = createTextBox(EditorSetupStrings.Artist, !string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist), - RomanisedArtistTextBox = createTextBox(EditorSetupMetadataStrings.RomanisedArtist, + RomanisedArtistTextBox = createTextBox(EditorSetupStrings.RomanisedArtist, !string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)), Empty(), - TitleTextBox = createTextBox(BeatmapsetWatchesStrings.IndexTableTitle, + TitleTextBox = createTextBox(EditorSetupStrings.Title, !string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title), - RomanisedTitleTextBox = createTextBox(EditorSetupMetadataStrings.RomanisedTitle, + RomanisedTitleTextBox = createTextBox(EditorSetupStrings.RomanisedTitle, !string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)), Empty(), - creatorTextBox = createTextBox(EditorSetupMetadataStrings.Creator, metadata.Author.Username), - difficultyTextBox = createTextBox(EditorSetupMetadataStrings.DifficultyName, Beatmap.BeatmapInfo.DifficultyName), + creatorTextBox = createTextBox(EditorSetupStrings.Creator, metadata.Author.Username), + difficultyTextBox = createTextBox(EditorSetupStrings.DifficultyName, Beatmap.BeatmapInfo.DifficultyName), sourceTextBox = createTextBox(BeatmapsetsStrings.ShowInfoSource, metadata.Source), tagsTextBox = createTextBox(BeatmapsetsStrings.ShowInfoTags, metadata.Tags) }; diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index dfc849de7b..efa50bf084 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.Edit.Setup private LabelledFileChooser audioTrackChooser; private LabelledFileChooser backgroundChooser; - public override LocalisableString Title => EditorSetupResourcesStrings.Resources; + public override LocalisableString Title => EditorSetupStrings.ResourcesHeader; [Resolved] private MusicController music { get; set; } @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Edit.Setup }, audioTrackChooser = new LabelledFileChooser(".mp3", ".ogg") { - Label = EditorSetupResourcesStrings.AudioTrack, + Label = EditorSetupStrings.AudioTrack, FixedLabelWidth = LABEL_WIDTH, TabbableContentContainer = this }, @@ -145,12 +145,12 @@ namespace osu.Game.Screens.Edit.Setup private void updatePlaceholderText() { audioTrackChooser.Text = audioTrackChooser.Current.Value == null - ? EditorSetupResourcesStrings.ClickToSelectTrack - : EditorSetupResourcesStrings.ClickToReplaceTrack; + ? EditorSetupStrings.ClickToSelectTrack + : EditorSetupStrings.ClickToReplaceTrack; backgroundChooser.Text = backgroundChooser.Current.Value == null - ? EditorSetupResourcesStrings.ClickToSelectBackground - : EditorSetupResourcesStrings.ClickToReplaceBackground; + ? EditorSetupStrings.ClickToSelectBackground + : EditorSetupStrings.ClickToReplaceBackground; } } } diff --git a/osu.Game/Screens/Edit/Setup/RulesetSetupSection.cs b/osu.Game/Screens/Edit/Setup/RulesetSetupSection.cs index 6b1b1128d4..d6664e860b 100644 --- a/osu.Game/Screens/Edit/Setup/RulesetSetupSection.cs +++ b/osu.Game/Screens/Edit/Setup/RulesetSetupSection.cs @@ -11,7 +11,7 @@ namespace osu.Game.Screens.Edit.Setup { public abstract class RulesetSetupSection : SetupSection { - public sealed override LocalisableString Title => EditorSetupRulesetStrings.Ruleset(rulesetInfo.Name); + public sealed override LocalisableString Title => EditorSetupStrings.RulesetHeader(rulesetInfo.Name); private readonly RulesetInfo rulesetInfo; From 3abc333813218a832ef3f2c229af4da5c52abbbf Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 15 Aug 2022 17:18:55 +0200 Subject: [PATCH 153/532] added hotkey for merging selection --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index f9d4fbfc72..e3cd31f6a3 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; @@ -18,6 +19,7 @@ using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit { @@ -62,6 +64,17 @@ namespace osu.Game.Rulesets.Osu.Edit referencePathTypes = null; } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed) + { + mergeSelection(); + return true; + } + + return false; + } + public override bool HandleMovement(MoveSelectionEvent moveEvent) { var hitObjects = selectedMovableObjects; From d261be873439cf0618188c25784bfdf38c8a7452 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 15 Aug 2022 17:19:04 +0200 Subject: [PATCH 154/532] added visual tests --- .../Editor/TestSceneObjectMerging.cs | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs new file mode 100644 index 0000000000..8387f9c74c --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -0,0 +1,179 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public class TestSceneObjectMerging : TestSceneOsuEditor + { + [Test] + public void TestSimpleMerge() + { + HitCircle circle1 = null; + HitCircle circle2 = null; + + AddStep("select first two circles", () => + { + circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle); + circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1); + EditorClock.Seek(circle1.StartTime); + EditorBeatmap.SelectedHitObjects.Add(circle1); + EditorBeatmap.SelectedHitObjects.Add(circle2); + }); + + mergeSelection(); + + AddAssert("slider created", () => sliderCreatedFor( + (pos: circle1.Position, pathType: PathType.Linear), + (pos: circle2.Position, pathType: null))); + + AddStep("undo", () => Editor.Undo()); + AddAssert("merged objects restored", () => objectsRestored(circle1, circle2)); + } + + [Test] + public void TestMergeCircleSlider() + { + HitCircle circle1 = null; + Slider slider = null; + HitCircle circle2 = null; + + AddStep("select a circle, slider, circle", () => + { + circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle); + slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > circle1.StartTime); + circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h.StartTime > slider.StartTime); + EditorClock.Seek(circle1.StartTime); + EditorBeatmap.SelectedHitObjects.Add(circle1); + EditorBeatmap.SelectedHitObjects.Add(slider); + EditorBeatmap.SelectedHitObjects.Add(circle2); + }); + + mergeSelection(); + + AddAssert("slider created", () => + { + var controlPoints = slider.Path.ControlPoints; + (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2]; + args[0] = (circle1.Position, PathType.Linear); + + for (int i = 0; i < controlPoints.Count; i++) + { + args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.Linear : controlPoints[i].Type); + } + + args[^1] = (circle2.Position, null); + return sliderCreatedFor(args); + }); + + AddStep("undo", () => Editor.Undo()); + AddAssert("merged objects restored", () => objectsRestored(circle1, slider, circle2)); + } + + [Test] + public void TestMergeSliderSlider() + { + Slider slider1 = null; + SliderPath slider1Path = null; + Slider slider2 = null; + + AddStep("select two sliders", () => + { + slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider); + slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value); + slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime); + EditorClock.Seek(slider1.StartTime); + EditorBeatmap.SelectedHitObjects.Add(slider1); + EditorBeatmap.SelectedHitObjects.Add(slider2); + }); + + mergeSelection(); + + AddAssert("slider created", () => + { + var controlPoints1 = slider1Path.ControlPoints; + var controlPoints2 = slider2.Path.ControlPoints; + (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1]; + + for (int i = 0; i < controlPoints1.Count - 1; i++) + { + args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type); + } + + for (int i = 0; i < controlPoints2.Count; i++) + { + args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type); + } + + return sliderCreatedFor(args); + }); + + AddAssert("merged slider matches first slider", () => + { + var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); + return mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples) + && mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples) + && mergedSlider.Samples.SequenceEqual(slider1.Samples) + && mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint); + }); + + AddAssert("slider end is at same completion for last slider", () => + { + var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); + return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance); + }); + } + + private void mergeSelection() + { + AddStep("merge selection", () => + { + InputManager.PressKey(Key.LControl); + InputManager.PressKey(Key.LShift); + InputManager.Key(Key.M); + InputManager.ReleaseKey(Key.LShift); + InputManager.ReleaseKey(Key.LControl); + }); + } + + private bool sliderCreatedFor(params (Vector2 pos, PathType? pathType)[] expectedControlPoints) + { + if (EditorBeatmap.SelectedHitObjects.Count != 1) + return false; + + var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); + int i = 0; + + foreach ((Vector2 pos, PathType? pathType) in expectedControlPoints) + { + var controlPoint = mergedSlider.Path.ControlPoints[i++]; + + if (!Precision.AlmostEquals(controlPoint.Position + mergedSlider.Position, pos) || controlPoint.Type != pathType) + return false; + } + + return true; + } + + private bool objectsRestored(params HitObject[] objects) + { + foreach (var hitObject in objects) + { + if (EditorBeatmap.HitObjects.Contains(hitObject)) + return false; + } + + return true; + } + } +} From b5e54113485d5c511bc179a67a2f0794cd78ec55 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 15 Aug 2022 18:07:55 +0200 Subject: [PATCH 155/532] remove copyright notice from new file --- osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 8387f9c74c..9b90c0ef05 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -1,6 +1,3 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - #nullable disable using System.Linq; From 5ff2e41a553be9a9dfcc6e6f47f1d99026818061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 14 Aug 2022 21:35:09 +0200 Subject: [PATCH 156/532] Add preset column to mod select test scene --- .../TestSceneModSelectOverlay.cs | 49 ++++++++++++------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 18 +++---- osu.Game/Screens/Select/SongSelect.cs | 2 +- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 07473aa55b..b3aa49ad55 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -1,14 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; @@ -29,10 +28,18 @@ namespace osu.Game.Tests.Visual.UserInterface [TestFixture] public class TestSceneModSelectOverlay : OsuManualInputManagerTestScene { - [Resolved] - private RulesetStore rulesetStore { get; set; } + protected override bool UseFreshStoragePerRun => true; - private UserModSelectOverlay modSelectOverlay; + private RulesetStore rulesetStore = null!; + + private TestModSelectOverlay modSelectOverlay = null!; + + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm)); + Dependencies.Cache(Realm); + } [SetUpSteps] public void SetUpSteps() @@ -44,7 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void createScreen() { - AddStep("create screen", () => Child = modSelectOverlay = new UserModSelectOverlay + AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, @@ -137,7 +144,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("any column dimmed", () => this.ChildrenOfType().Any(column => !column.Active.Value)); - ModSelectColumn lastColumn = null; + ModSelectColumn lastColumn = null!; AddAssert("last column dimmed", () => !this.ChildrenOfType().Last().Active.Value); AddStep("request scroll to last column", () => @@ -170,7 +177,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("dismiss mod customisation via toggle", () => { - InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().Single()); + InputManager.MoveMouseTo(modSelectOverlay.CustomisationButton); InputManager.Click(MouseButton.Left); }); assertCustomisationToggleState(disabled: false, active: false); @@ -224,8 +231,8 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestSettingsNotCrossPolluting() { - Bindable> selectedMods2 = null; - ModSelectOverlay modSelectOverlay2 = null; + Bindable> selectedMods2 = null!; + ModSelectOverlay modSelectOverlay2 = null!; createScreen(); AddStep("select diff adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }); @@ -353,7 +360,7 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestExternallySetModIsReplacedByOverlayInstance() { Mod external = new OsuModDoubleTime(); - Mod overlayButtonMod = null; + Mod overlayButtonMod = null!; createScreen(); changeRuleset(0); @@ -460,12 +467,12 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select difficulty adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }); assertCustomisationToggleState(disabled: false, active: true); - AddAssert("back button disabled", () => !this.ChildrenOfType().First().Enabled.Value); + AddAssert("back button disabled", () => !modSelectOverlay.BackButton.Enabled.Value); AddStep("dismiss customisation area", () => InputManager.Key(Key.Escape)); AddStep("click back button", () => { - InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.MoveMouseTo(modSelectOverlay.BackButton); InputManager.Click(MouseButton.Left); }); AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); @@ -474,7 +481,7 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestColumnHiding() { - AddStep("create screen", () => Child = modSelectOverlay = new UserModSelectOverlay + AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible }, @@ -527,15 +534,21 @@ namespace osu.Game.Tests.Visual.UserInterface private void assertCustomisationToggleState(bool disabled, bool active) { - ShearedToggleButton getToggle() => modSelectOverlay.ChildrenOfType().Single(); - - AddAssert($"customisation toggle is {(disabled ? "" : "not ")}disabled", () => getToggle().Active.Disabled == disabled); - AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => getToggle().Active.Value == active); + AddAssert($"customisation toggle is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.CustomisationButton.AsNonNull().Active.Disabled == disabled); + AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => modSelectOverlay.CustomisationButton.AsNonNull().Active.Value == active); } private ModPanel getPanelForMod(Type modType) => modSelectOverlay.ChildrenOfType().Single(panel => panel.Mod.GetType() == modType); + private class TestModSelectOverlay : UserModSelectOverlay + { + protected override bool ShowPresets => true; + + public new ShearedButton BackButton => base.BackButton; + public new ShearedToggleButton? CustomisationButton => base.CustomisationButton; + } + private class TestUnimplementedMod : Mod { public override string Name => "Unimplemented mod"; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index adc008e1f7..5973b919e5 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -87,7 +87,7 @@ namespace osu.Game.Overlays.Mods { if (AllowCustomisation) { - yield return customisationButton = new ShearedToggleButton(BUTTON_WIDTH) + yield return CustomisationButton = new ShearedToggleButton(BUTTON_WIDTH) { Text = ModSelectOverlayStrings.ModCustomisation, Active = { BindTarget = customisationVisible } @@ -107,11 +107,11 @@ namespace osu.Game.Overlays.Mods private ColumnScrollContainer columnScroll = null!; private ColumnFlowContainer columnFlow = null!; private FillFlowContainer footerButtonFlow = null!; - private ShearedButton backButton = null!; private DifficultyMultiplierDisplay? multiplierDisplay; - private ShearedToggleButton? customisationButton; + protected ShearedButton BackButton { get; private set; } = null!; + protected ShearedToggleButton? CustomisationButton { get; private set; } private Sample? columnAppearSample; @@ -214,7 +214,7 @@ namespace osu.Game.Overlays.Mods Horizontal = 70 }, Spacing = new Vector2(10), - ChildrenEnumerable = CreateFooterButtons().Prepend(backButton = new ShearedButton(BUTTON_WIDTH) + ChildrenEnumerable = CreateFooterButtons().Prepend(BackButton = new ShearedButton(BUTTON_WIDTH) { Text = CommonStrings.Back, Action = Hide, @@ -358,7 +358,7 @@ namespace osu.Game.Overlays.Mods private void updateCustomisation(ValueChangedEvent> valueChangedEvent) { - if (customisationButton == null) + if (CustomisationButton == null) return; bool anyCustomisableMod = false; @@ -394,7 +394,7 @@ namespace osu.Game.Overlays.Mods foreach (var button in footerButtonFlow) { - if (button != customisationButton) + if (button != CustomisationButton) button.Enabled.Value = !customisationVisible.Value; } @@ -587,14 +587,14 @@ namespace osu.Game.Overlays.Mods { if (customisationVisible.Value) { - Debug.Assert(customisationButton != null); - customisationButton.TriggerClick(); + Debug.Assert(CustomisationButton != null); + CustomisationButton.TriggerClick(); if (!immediate) return; } - backButton.TriggerClick(); + BackButton.TriggerClick(); } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 33ff31857f..d4e4b09303 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -928,7 +928,7 @@ namespace osu.Game.Screens.Select } } - private class SoloModSelectOverlay : UserModSelectOverlay + internal class SoloModSelectOverlay : UserModSelectOverlay { protected override bool ShowPresets => true; } From f860bc11eedcb928f2112e7605f7b025862ece64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Aug 2022 20:34:09 +0200 Subject: [PATCH 157/532] Fix several schedule-related issues arising from new column addition --- osu.Game/Overlays/Mods/ModColumn.cs | 7 ++++++- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 13 ++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 7a2c727a00..cf123f0347 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -66,7 +66,10 @@ namespace osu.Game.Overlays.Mods private IModHotkeyHandler hotkeyHandler = null!; private Task? latestLoadTask; - internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true; + private bool itemsLoaded; + internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true && itemsLoaded; + + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; public ModColumn(ModType modType, bool allowIncompatibleSelection) { @@ -132,10 +135,12 @@ namespace osu.Game.Overlays.Mods var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero)); + itemsLoaded = false; latestLoadTask = LoadComponentsAsync(panels, loaded => { ItemsFlow.ChildrenEnumerable = loaded; updateState(); + itemsLoaded = true; }, (cancellationTokenSource = new CancellationTokenSource()).Token); } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 5973b919e5..cd3b5bacec 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -708,7 +708,18 @@ namespace osu.Game.Overlays.Mods FinishTransforms(); } - protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || (Column as ModColumn)?.SelectionAnimationRunning == true; + protected override bool RequiresChildrenUpdate + { + get + { + bool result = base.RequiresChildrenUpdate; + + if (Column is ModColumn modColumn) + result |= !modColumn.ItemsLoaded || modColumn.SelectionAnimationRunning; + + return result; + } + } private void updateState() { From f0ad31b65097aba89467643c04c6b11cf62b2e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Aug 2022 18:44:51 +0200 Subject: [PATCH 158/532] Add failing test case --- .../TestSceneModSelectOverlay.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index b3aa49ad55..f17f93a875 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -47,6 +47,26 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("clear contents", Clear); AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0)); AddStep("reset mods", () => SelectedMods.SetDefault()); + AddStep("set up presets", () => + { + Realm.Write(r => + { + r.RemoveAll(); + r.Add(new ModPreset + { + Name = "AR0", + Description = "Too... many... circles...", + Ruleset = r.Find(OsuRuleset.SHORT_NAME), + Mods = new[] + { + new OsuModDifficultyAdjust + { + ApproachRate = { Value = 0 } + } + } + }); + }); + }); } private void createScreen() @@ -200,6 +220,13 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() }); assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action. + + AddStep("select mod preset with mod requiring configuration", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + assertCustomisationToggleState(disabled: false, active: false); } [Test] From 10daac675294d6c9fa894af5b0521de218f30253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Aug 2022 19:40:05 +0200 Subject: [PATCH 159/532] Only open mod customisation panel on explicit selection of single mod --- osu.Game/Overlays/Mods/ModPanel.cs | 16 ++++++++++++++++ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 19 ++++++++++--------- osu.Game/Overlays/Mods/ModState.cs | 7 +++++++ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 6ef6ab0595..0ab6293faf 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -37,6 +37,8 @@ namespace osu.Game.Overlays.Mods Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE) }; + + Action = select; } public ModPanel(Mod mod) @@ -57,6 +59,20 @@ namespace osu.Game.Overlays.Mods Filtered.BindValueChanged(_ => updateFilterState(), true); } + private void select() + { + if (!Active.Value) + { + modState.RequiresConfiguration = Mod.RequiresConfiguration; + Active.Value = true; + } + else + { + modState.RequiresConfiguration = false; + Active.Value = false; + } + } + #region Filtering support private void updateFilterState() diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index cd3b5bacec..66ecb9fd78 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -247,8 +247,8 @@ namespace osu.Game.Overlays.Mods modSettingChangeTracker?.Dispose(); updateMultiplier(); - updateCustomisation(val); updateFromExternalSelection(); + updateCustomisation(); if (AllowCustomisation) { @@ -356,25 +356,26 @@ namespace osu.Game.Overlays.Mods multiplierDisplay.Current.Value = multiplier; } - private void updateCustomisation(ValueChangedEvent> valueChangedEvent) + private void updateCustomisation() { if (CustomisationButton == null) return; - bool anyCustomisableMod = false; - bool anyModWithRequiredCustomisationAdded = false; + bool anyCustomisableModActive = false; + bool anyModRequiresCustomisation = false; - foreach (var mod in SelectedMods.Value) + foreach (var modState in allAvailableMods) { - anyCustomisableMod |= mod.GetSettingsSourceProperties().Any(); - anyModWithRequiredCustomisationAdded |= valueChangedEvent.OldValue.All(m => m.GetType() != mod.GetType()) && mod.RequiresConfiguration; + anyCustomisableModActive |= modState.Active.Value && modState.Mod.GetSettingsSourceProperties().Any(); + anyModRequiresCustomisation |= modState.RequiresConfiguration; + modState.RequiresConfiguration = false; } - if (anyCustomisableMod) + if (anyCustomisableModActive) { customisationVisible.Disabled = false; - if (anyModWithRequiredCustomisationAdded && !customisationVisible.Value) + if (anyModRequiresCustomisation && !customisationVisible.Value) customisationVisible.Value = true; } else diff --git a/osu.Game/Overlays/Mods/ModState.cs b/osu.Game/Overlays/Mods/ModState.cs index 79880b85a5..a3806b1936 100644 --- a/osu.Game/Overlays/Mods/ModState.cs +++ b/osu.Game/Overlays/Mods/ModState.cs @@ -24,6 +24,13 @@ namespace osu.Game.Overlays.Mods /// public BindableBool Active { get; } = new BindableBool(); + /// + /// Whether the mod requires further customisation. + /// This flag is read by the to determine if the customisation panel should be opened after a mod change + /// and cleared after reading. + /// + public bool RequiresConfiguration { get; set; } + /// /// Whether the mod is currently filtered out due to not matching imposed criteria. /// From a494e55d938c009e6917f673ca43919c1f2d2300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 15 Aug 2022 19:46:17 +0200 Subject: [PATCH 160/532] Adjust test scene to reflect new behaviour --- .../UserInterface/TestSceneModSelectOverlay.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index f17f93a875..6f9edb1b8a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -192,7 +192,11 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select customisable mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() }); assertCustomisationToggleState(disabled: false, active: false); - AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); + AddStep("select mod requiring configuration externally", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); + assertCustomisationToggleState(disabled: false, active: false); + + AddStep("reset mods", () => SelectedMods.SetDefault()); + AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); assertCustomisationToggleState(disabled: false, active: true); AddStep("dismiss mod customisation via toggle", () => @@ -203,7 +207,7 @@ namespace osu.Game.Tests.Visual.UserInterface assertCustomisationToggleState(disabled: false, active: false); AddStep("reset mods", () => SelectedMods.SetDefault()); - AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); + AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); assertCustomisationToggleState(disabled: false, active: true); AddStep("dismiss mod customisation via keyboard", () => InputManager.Key(Key.Escape)); @@ -215,7 +219,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() }); assertCustomisationToggleState(disabled: true, active: false); - AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); + AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); assertCustomisationToggleState(disabled: false, active: true); AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() }); @@ -235,7 +239,7 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); assertCustomisationToggleState(disabled: true, active: false); - AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); + AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); assertCustomisationToggleState(disabled: false, active: true); AddStep("move mouse to settings area", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); @@ -262,7 +266,7 @@ namespace osu.Game.Tests.Visual.UserInterface ModSelectOverlay modSelectOverlay2 = null!; createScreen(); - AddStep("select diff adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }); + AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); AddStep("set setting", () => modSelectOverlay.ChildrenOfType>().First().Current.Value = 8); @@ -492,7 +496,7 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); changeRuleset(0); - AddStep("select difficulty adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }); + AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); assertCustomisationToggleState(disabled: false, active: true); AddAssert("back button disabled", () => !modSelectOverlay.BackButton.Enabled.Value); From 7c2ada9b209f1a887e026aac48c9cadeb3d7905f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 15 Aug 2022 21:11:06 +0200 Subject: [PATCH 161/532] Revert "remove copyright notice from new file" This reverts commit b5e54113485d5c511bc179a67a2f0794cd78ec55. --- osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 9b90c0ef05..8387f9c74c 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + #nullable disable using System.Linq; From d140e0df14224da4163c9e5049aa3eec0bf5ce1d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 15 Aug 2022 21:15:23 +0200 Subject: [PATCH 162/532] remove nullable disable annotation --- .../Editor/TestSceneObjectMerging.cs | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 8387f9c74c..63e6760514 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Utils; @@ -19,8 +17,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestSimpleMerge() { - HitCircle circle1 = null; - HitCircle circle2 = null; + HitCircle? circle1 = null; + HitCircle? circle2 = null; AddStep("select first two circles", () => { @@ -33,20 +31,20 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor mergeSelection(); - AddAssert("slider created", () => sliderCreatedFor( + AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( (pos: circle1.Position, pathType: PathType.Linear), (pos: circle2.Position, pathType: null))); AddStep("undo", () => Editor.Undo()); - AddAssert("merged objects restored", () => objectsRestored(circle1, circle2)); + AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && objectsRestored(circle1, circle2)); } [Test] public void TestMergeCircleSlider() { - HitCircle circle1 = null; - Slider slider = null; - HitCircle circle2 = null; + HitCircle? circle1 = null; + Slider? slider = null; + HitCircle? circle2 = null; AddStep("select a circle, slider, circle", () => { @@ -63,6 +61,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider created", () => { + if (circle1 is null || circle2 is null || slider is null) + return false; + var controlPoints = slider.Path.ControlPoints; (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2]; args[0] = (circle1.Position, PathType.Linear); @@ -77,15 +78,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); AddStep("undo", () => Editor.Undo()); - AddAssert("merged objects restored", () => objectsRestored(circle1, slider, circle2)); + AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && slider is not null && objectsRestored(circle1, slider, circle2)); } [Test] public void TestMergeSliderSlider() { - Slider slider1 = null; - SliderPath slider1Path = null; - Slider slider2 = null; + Slider? slider1 = null; + SliderPath? slider1Path = null; + Slider? slider2 = null; AddStep("select two sliders", () => { @@ -101,6 +102,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider created", () => { + if (slider1 is null || slider2 is null || slider1Path is null) + return false; + var controlPoints1 = slider1Path.ControlPoints; var controlPoints2 = slider2.Path.ControlPoints; (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1]; @@ -121,14 +125,17 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("merged slider matches first slider", () => { var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); - return mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples) - && mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples) - && mergedSlider.Samples.SequenceEqual(slider1.Samples) - && mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint); + return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples) + && mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples) + && mergedSlider.Samples.SequenceEqual(slider1.Samples) + && mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint); }); AddAssert("slider end is at same completion for last slider", () => { + if (slider1Path is null || slider2 is null) + return false; + var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance); }); From b33e0f5e1cd3421503eacfc6283210cd2be7c0d6 Mon Sep 17 00:00:00 2001 From: 63411 <62799417+molneya@users.noreply.github.com> Date: Tue, 16 Aug 2022 11:03:18 +0800 Subject: [PATCH 163/532] update comment and deltaTime check --- osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index 657dfbb575..d5cee47178 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -78,8 +78,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base); individualStrains[column] += 2.0 * holdFactor; - // individualStrain should be the hardest individualStrain column for notes in a chord - individualStrain = maniaCurrent.DeltaTime == 0 ? Math.Max(individualStrain, individualStrains[column]) : individualStrains[column]; + // For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns + individualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(individualStrain, individualStrains[column]) : individualStrains[column]; // Decay and increase overallStrain overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base); From a15e6f19aa48d2ce7ec39387f0373cc4d7bde144 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 13:35:57 +0900 Subject: [PATCH 164/532] Fix running `TestScenePlayerLoader` interactively leaving volume in a bad state --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 81 +++++++++++++++---- 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 05474e3d39..dbbedf37ae 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -56,6 +56,10 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly ChangelogOverlay changelogOverlay; + private double savedTrackVolume; + private double savedMasterVolume; + private bool savedMutedState; + public TestScenePlayerLoader() { AddRange(new Drawable[] @@ -75,11 +79,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [SetUp] - public void Setup() => Schedule(() => - { - player = null; - audioManager.Volume.SetDefault(); - }); + public void Setup() => Schedule(() => player = null); /// /// Sets the input manager child to a new test player loader container instance. @@ -147,6 +147,7 @@ namespace osu.Game.Tests.Visual.Gameplay moveMouse(); return player?.LoadState == LoadState.Ready; }); + AddRepeatStep("move mouse", moveMouse, 20); AddAssert("loader still active", () => loader.IsCurrentScreen()); @@ -154,6 +155,8 @@ namespace osu.Game.Tests.Visual.Gameplay void moveMouse() { + notificationOverlay.State.Value = Visibility.Hidden; + InputManager.MoveMouseTo( loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft + (loader.VisualSettings.ScreenSpaceDrawQuad.BottomRight - loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft) @@ -274,6 +277,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("load player", () => resetPlayer(false, beforeLoad)); AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); + saveVolumes(); + AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == 1); AddStep("click notification", () => { @@ -287,6 +292,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("check " + volumeName, assert); + restoreVolumes(); + AddUntilStep("wait for player load", () => player.IsLoaded); } @@ -294,6 +301,9 @@ namespace osu.Game.Tests.Visual.Gameplay [TestCase(false)] public void TestEpilepsyWarning(bool warning) { + saveVolumes(); + setFullVolume(); + AddStep("change epilepsy warning", () => epilepsyWarning = warning); AddStep("load dummy beatmap", () => resetPlayer(false)); @@ -306,6 +316,30 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("sound volume decreased", () => Beatmap.Value.Track.AggregateVolume.Value == 0.25); AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); } + + restoreVolumes(); + } + + [Test] + public void TestEpilepsyWarningEarlyExit() + { + saveVolumes(); + setFullVolume(); + + AddStep("set epilepsy warning", () => epilepsyWarning = true); + AddStep("load dummy beatmap", () => resetPlayer(false)); + + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + + AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0); + AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible); + + AddStep("exit early", () => loader.Exit()); + + AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden); + AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); + + restoreVolumes(); } [TestCase(true, 1.0, false)] // on battery, above cutoff --> no warning @@ -336,21 +370,34 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for player load", () => player.IsLoaded); } - [Test] - public void TestEpilepsyWarningEarlyExit() + private void restoreVolumes() { - AddStep("set epilepsy warning", () => epilepsyWarning = true); - AddStep("load dummy beatmap", () => resetPlayer(false)); + AddStep("restore previous volumes", () => + { + audioManager.VolumeTrack.Value = savedTrackVolume; + audioManager.Volume.Value = savedMasterVolume; + volumeOverlay.IsMuted.Value = savedMutedState; + }); + } - AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + private void setFullVolume() + { + AddStep("set volumes to 100%", () => + { + audioManager.VolumeTrack.Value = 1; + audioManager.Volume.Value = 1; + volumeOverlay.IsMuted.Value = false; + }); + } - AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0); - AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible); - - AddStep("exit early", () => loader.Exit()); - - AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden); - AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); + private void saveVolumes() + { + AddStep("save previous volumes", () => + { + savedTrackVolume = audioManager.VolumeTrack.Value; + savedMasterVolume = audioManager.Volume.Value; + savedMutedState = volumeOverlay.IsMuted.Value; + }); } private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(); From 6761f869f993493598f9de7d2d192b1ba7cef8ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 13:04:56 +0900 Subject: [PATCH 165/532] Modify flow to avoid weird bindable and value resetting --- .../Visual/Mods/TestSceneModFailCondition.cs | 2 +- osu.Game/Screens/Play/Player.cs | 21 +++++++------------ osu.Game/Screens/Play/PlayerLoader.cs | 15 ++++--------- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- 4 files changed, 14 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs b/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs index 91fa09b414..23c1eda7f7 100644 --- a/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs +++ b/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Mods protected override TestPlayer CreateModPlayer(Ruleset ruleset) { var player = base.CreateModPlayer(ruleset); - player.RestartRequested = () => restartRequested = true; + player.RestartRequested = _ => restartRequested = true; return player; } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 7ed68aa5b7..4f6bb1d37a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -77,15 +77,10 @@ namespace osu.Game.Screens.Play /// protected virtual bool PauseOnFocusLost => true; - public Action RestartRequested; + public Action RestartRequested; private bool isRestarting; - /// - /// Is set to true when the quick retry hotkey has been pressed. - /// - public Bindable IsQuickRestart = new Bindable(); - private Bindable mouseWheelDisabled; private readonly Bindable storyboardReplacesBackground = new Bindable(); @@ -272,7 +267,7 @@ namespace osu.Game.Screens.Play FailOverlay = new FailOverlay { SaveReplay = prepareAndImportScore, - OnRetry = Restart, + OnRetry = () => Restart(), OnQuit = () => PerformExit(true), }, new HotkeyExitOverlay @@ -298,9 +293,8 @@ namespace osu.Game.Screens.Play { if (!this.IsCurrentScreen()) return; - IsQuickRestart.Value = true; fadeOut(true); - Restart(); + Restart(true); }, }); } @@ -453,7 +447,7 @@ namespace osu.Game.Screens.Play { OnResume = Resume, Retries = RestartCount, - OnRetry = Restart, + OnRetry = () => Restart(), OnQuit = () => PerformExit(true), }, }, @@ -660,7 +654,8 @@ namespace osu.Game.Screens.Play /// Restart gameplay via a parent . /// This can be called from a child screen in order to trigger the restart process. /// - public void Restart() + /// Whether a quick restart was requested (skipping intro etc.). + public void Restart(bool quickRestart = false) { if (!Configuration.AllowRestart) return; @@ -672,7 +667,7 @@ namespace osu.Game.Screens.Play musicController.Stop(); sampleRestart?.Play(); - RestartRequested?.Invoke(); + RestartRequested?.Invoke(quickRestart); PerformExit(false); } @@ -852,7 +847,7 @@ namespace osu.Game.Screens.Play failAnimationLayer.Start(); if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) - Restart(); + Restart(true); return true; } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 76092fbaa3..e6bd1367ef 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Play private EpilepsyWarning? epilepsyWarning; - private bool isHotKeyRestart; + private bool quickRestart; [Resolved(CanBeNull = true)] private INotificationOverlay? notificationOverlay { get; set; } @@ -363,17 +363,12 @@ namespace osu.Game.Screens.Play return; CurrentPlayer = createPlayer(); + CurrentPlayer.Configuration.AutomaticallySkipIntro = quickRestart; CurrentPlayer.RestartCount = restartCount++; CurrentPlayer.RestartRequested = restartRequested; LoadTask = LoadComponentAsync(CurrentPlayer, _ => { - if (isHotKeyRestart) - { - CurrentPlayer.Configuration.AutomaticallySkipIntro = true; - isHotKeyRestart = false; - } - MetadataInfo.Loading = false; OnPlayerLoaded(); }); @@ -383,11 +378,9 @@ namespace osu.Game.Screens.Play { } - private void restartRequested() + private void restartRequested(bool quickRestartRequested) { - if (CurrentPlayer != null) - isHotKeyRestart = CurrentPlayer.IsQuickRestart.Value; - + quickRestart = quickRestartRequested; hideOverlays = true; ValidForResume = true; } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index c530febcae..226216b0f0 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -177,7 +177,7 @@ namespace osu.Game.Screens.Ranking { if (!this.IsCurrentScreen()) return; - player?.Restart(); + player?.Restart(true); }, }); } From 9a1a7bae891c1b13a90ccf72354ee1fad6a1e6fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 14:17:28 +0900 Subject: [PATCH 166/532] Make test actually test things --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 38 +++++++++++++++---- osu.Game/Screens/Play/SkipOverlay.cs | 2 + 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index b0308dcd22..1fd517d1a6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -29,6 +29,7 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Utils; using osuTK.Input; +using SkipOverlay = osu.Game.Screens.Play.SkipOverlay; namespace osu.Game.Tests.Visual.Gameplay { @@ -98,7 +99,13 @@ namespace osu.Game.Tests.Visual.Gameplay private void prepareBeatmap() { var workingBeatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + + // Add intro time to test quick retry skipping (TestQuickRetry). + workingBeatmap.BeatmapInfo.AudioLeadIn = 60000; + + // Turn on epilepsy warning to test warning display (TestEpilepsyWarning). workingBeatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning; + Beatmap.Value = workingBeatmap; foreach (var mod in SelectedMods.Value.OfType()) @@ -356,17 +363,32 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickRetry() { + TestPlayer getCurrentPlayer() => loader.CurrentPlayer as TestPlayer; + bool checkSkipButtonVisible() => player.ChildrenOfType().FirstOrDefault()?.IsButtonVisible == true; + + TestPlayer previousPlayer = null; + AddStep("load dummy beatmap", () => resetPlayer(false)); - AddUntilStep("wait for current", () => player.IsCurrentScreen()); - AddStep("Restart map normally", () => player.Restart()); - AddUntilStep("wait for current", () => player.IsCurrentScreen()); + AddUntilStep("wait for current", () => getCurrentPlayer()?.IsCurrentScreen() == true); + AddStep("store previous player", () => previousPlayer = getCurrentPlayer()); - AddStep("Restart map with quick retry hotkey", () => - { - InputManager.UseParentInput = true; - InputManager.PressKey(Key.Tilde); - }); + AddStep("Restart map normally", () => getCurrentPlayer().Restart()); + AddUntilStep("wait for load", () => getCurrentPlayer()?.LoadedBeatmapSuccessfully == true); + + AddUntilStep("restart completed", () => getCurrentPlayer() != null && getCurrentPlayer() != previousPlayer); + AddStep("store previous player", () => previousPlayer = getCurrentPlayer()); + + AddUntilStep("skip button visible", checkSkipButtonVisible); + + AddStep("press quick retry key", () => InputManager.PressKey(Key.Tilde)); + AddUntilStep("restart completed", () => getCurrentPlayer() != null && getCurrentPlayer() != previousPlayer); + AddStep("release quick retry key", () => InputManager.ReleaseKey(Key.Tilde)); + + AddUntilStep("wait for load", () => getCurrentPlayer()?.LoadedBeatmapSuccessfully == true); + + AddUntilStep("time reached zero", () => getCurrentPlayer()?.GameplayClockContainer.CurrentTime > 0); + AddUntilStep("skip button not visible", () => !checkSkipButtonVisible()); } private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(); diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 70a8345c1f..0629d0ae23 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -46,6 +46,8 @@ namespace osu.Game.Screens.Play [Resolved] private IGameplayClock gameplayClock { get; set; } + internal bool IsButtonVisible => fadeContainer.State == Visibility.Visible && buttonContainer.State.Value == Visibility.Visible; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; /// From 63819648dfda22157edd7c029cea5a6db06b8235 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 14:27:02 +0900 Subject: [PATCH 167/532] Fix up flow of actual skip operation --- osu.Game/Screens/Play/Player.cs | 7 ++----- osu.Game/Screens/Play/SkipOverlay.cs | 20 +++++++++++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4f6bb1d37a..6827ff04d3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -372,11 +372,8 @@ namespace osu.Game.Screens.Play IsBreakTime.BindTo(breakTracker.IsBreakTime); IsBreakTime.BindValueChanged(onBreakTimeChanged, true); - skipIntroOverlay.IsSkippable.ValueChanged += e => - { - if (Configuration.AutomaticallySkipIntro && e.NewValue && RestartCount > 0) - performUserRequestedSkip(); - }; + if (Configuration.AutomaticallySkipIntro) + skipIntroOverlay.SkipWhenReady(); } protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 0629d0ae23..5c9a706549 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -8,7 +8,6 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -40,8 +39,7 @@ namespace osu.Game.Screens.Play private double displayTime; private bool isClickable; - - public IBindable IsSkippable = new Bindable(); + private bool skipQueued; [Resolved] private IGameplayClock gameplayClock { get; set; } @@ -94,8 +92,6 @@ namespace osu.Game.Screens.Play } } }; - - IsSkippable.BindTo(button.Enabled); } private const double fade_time = 300; @@ -130,6 +126,20 @@ namespace osu.Game.Screens.Play displayTime = gameplayClock.CurrentTime; fadeContainer.TriggerShow(); + + if (skipQueued) + { + Scheduler.AddDelayed(() => button.TriggerClick(), 200); + skipQueued = false; + } + } + + public void SkipWhenReady() + { + if (IsLoaded) + button.TriggerClick(); + else + skipQueued = true; } protected override void Update() From e870ac6456c334577f4cc57fff54305179cae041 Mon Sep 17 00:00:00 2001 From: its5Q Date: Tue, 16 Aug 2022 15:51:54 +1000 Subject: [PATCH 168/532] Fix code quality for CI --- osu.Game/Screens/Edit/Setup/ColoursSection.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/ColoursSection.cs b/osu.Game/Screens/Edit/Setup/ColoursSection.cs index 607086fdba..9334967a3e 100644 --- a/osu.Game/Screens/Edit/Setup/ColoursSection.cs +++ b/osu.Game/Screens/Edit/Setup/ColoursSection.cs @@ -26,7 +26,8 @@ namespace osu.Game.Screens.Edit.Setup { Label = EditorSetupStrings.HitcircleSliderCombos, FixedLabelWidth = LABEL_WIDTH, - ColourNamePrefix = EditorSetupStrings.ComboColourPrefix } + ColourNamePrefix = EditorSetupStrings.ComboColourPrefix + } }; if (Beatmap.BeatmapSkin != null) From ea50936d718bd21a78fea574811cc7b560a651f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 15:26:29 +0900 Subject: [PATCH 169/532] Fix slider ticks playing back at infinite rate while making changes to a slider in the editor --- osu.Game/Screens/Edit/Editor.cs | 19 ++++++------------- osu.Game/Screens/Edit/EditorBeatmap.cs | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 89f9aec5ee..90e912bd6d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -225,8 +225,6 @@ namespace osu.Game.Screens.Edit dependencies.CacheAs(clock); AddInternal(clock); - clock.SeekingOrStopped.BindValueChanged(_ => updateSampleDisabledState()); - // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); @@ -428,7 +426,12 @@ namespace osu.Game.Screens.Edit protected override void Update() { base.Update(); + clock.ProcessFrame(); + + samplePlaybackDisabled.Value = clock.SeekingOrStopped.Value + || currentScreen is not ComposeScreen + || editorBeatmap.UpdateInProgress; } public bool OnPressed(KeyBindingPressEvent e) @@ -822,16 +825,10 @@ namespace osu.Game.Screens.Edit } finally { - updateSampleDisabledState(); rebindClipboardBindables(); } } - private void updateSampleDisabledState() - { - samplePlaybackDisabled.Value = clock.SeekingOrStopped.Value || !(currentScreen is ComposeScreen); - } - private void seek(UIEvent e, int direction) { double amount = e.ShiftPressed ? 4 : 1; @@ -936,11 +933,7 @@ namespace osu.Game.Screens.Edit protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset)); - private void cancelExit() - { - updateSampleDisabledState(); - loader?.CancelPendingDifficultySwitch(); - } + private void cancelExit() => loader?.CancelPendingDifficultySwitch(); public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 96425e8bc8..b9b0879fa4 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -10,6 +10,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; @@ -22,6 +23,15 @@ namespace osu.Game.Screens.Edit { public class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBeatSnapProvider { + /// + /// While performing updates on hitobjects, this will momentarily become true. + /// + /// + /// This is intended to be used to avoid performing operations (like playback of samples) + /// while mutating hitobjects. + /// + public bool UpdateInProgress { get; private set; } + /// /// Invoked when a is added to this . /// @@ -225,8 +235,12 @@ namespace osu.Game.Screens.Edit { // updates are debounced regardless of whether a batch is active. batchPendingUpdates.Add(hitObject); + + advertiseUpdateInProgress(); } + private ScheduledDelegate updateCompleteDelegate; + /// /// Update all hit objects with potentially changed difficulty or control point data. /// @@ -234,6 +248,8 @@ namespace osu.Game.Screens.Edit { foreach (var h in HitObjects) batchPendingUpdates.Add(h); + + advertiseUpdateInProgress(); } /// @@ -333,6 +349,15 @@ namespace osu.Game.Screens.Edit /// public void Clear() => RemoveRange(HitObjects.ToArray()); + private void advertiseUpdateInProgress() + { + UpdateInProgress = true; + + // Debounce is arbitrarily high enough to avoid flip-flopping the value each other frame. + updateCompleteDelegate?.Cancel(); + updateCompleteDelegate = Scheduler.AddDelayed(() => UpdateInProgress = false, 50); + } + private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, PlayableBeatmap.Difficulty); private void trackStartTime(HitObject hitObject) From e636fcd9b8312be89cf884ca98066becf4720288 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 15:35:32 +0900 Subject: [PATCH 170/532] Use DI'd components from parent class rather than fetching locally --- .../Edit/OsuSelectionHandler.cs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index e3cd31f6a3..44fbbd9591 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -16,7 +16,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; -using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; using osuTK.Input; @@ -28,12 +27,6 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved(CanBeNull = true)] private IDistanceSnapProvider? snapProvider { get; set; } - [Resolved(CanBeNull = true)] - private EditorBeatmap? editorBeatmap { get; set; } - - [Resolved(CanBeNull = true)] - private IEditorChangeHandler? changeHandler { get; set; } - /// /// During a transform, the initial origin is stored so it can be used throughout the operation. /// @@ -355,10 +348,10 @@ namespace osu.Game.Rulesets.Osu.Edit private void mergeSelection() { - if (editorBeatmap == null || changeHandler == null || selectedMergeableObjects.Length < 2) + if (EditorBeatmap == null || ChangeHandler == null || selectedMergeableObjects.Length < 2) return; - changeHandler.BeginChange(); + ChangeHandler.BeginChange(); // Have an initial slider object. var firstHitObject = selectedMergeableObjects[0]; @@ -416,24 +409,24 @@ namespace osu.Game.Rulesets.Osu.Edit { foreach (var selectedMergeableObject in selectedMergeableObjects.Skip(1)) { - editorBeatmap.Remove(selectedMergeableObject); + EditorBeatmap.Remove(selectedMergeableObject); } } else { foreach (var selectedMergeableObject in selectedMergeableObjects) { - editorBeatmap.Remove(selectedMergeableObject); + EditorBeatmap.Remove(selectedMergeableObject); } - editorBeatmap.Add(mergedHitObject); + EditorBeatmap.Add(mergedHitObject); } // Make sure the merged hitobject is selected. SelectedItems.Clear(); SelectedItems.Add(mergedHitObject); - changeHandler.EndChange(); + ChangeHandler.EndChange(); } protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) From ecb9351babd55217c0b28a5e20c3274be44669b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 15:36:43 +0900 Subject: [PATCH 171/532] Remove unnecessary null pre-checks --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 44fbbd9591..363a191e34 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -348,10 +348,10 @@ namespace osu.Game.Rulesets.Osu.Edit private void mergeSelection() { - if (EditorBeatmap == null || ChangeHandler == null || selectedMergeableObjects.Length < 2) + if (selectedMergeableObjects.Length < 2) return; - ChangeHandler.BeginChange(); + ChangeHandler?.BeginChange(); // Have an initial slider object. var firstHitObject = selectedMergeableObjects[0]; @@ -426,7 +426,7 @@ namespace osu.Game.Rulesets.Osu.Edit SelectedItems.Clear(); SelectedItems.Add(mergedHitObject); - ChangeHandler.EndChange(); + ChangeHandler?.EndChange(); } protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) From 7cc9fdbaa0d19f847cdc57264bc6652746ac91b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 15:37:38 +0900 Subject: [PATCH 172/532] Simplify context menu check by using existing mergeable object list --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 363a191e34..663bf03a00 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -335,7 +335,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// All osu! hitobjects which can be moved/rotated/scaled. /// private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType() - .Where(h => !(h is Spinner)) + .Where(h => h is not Spinner) .ToArray(); /// @@ -434,7 +434,7 @@ namespace osu.Game.Rulesets.Osu.Edit foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; - if (selection.Count() > 1 && selection.All(o => o.Item is HitCircle or Slider)) + if (selectedMergeableObjects.Length > 1) yield return new OsuMenuItem("Merge selection", MenuItemType.Destructive, mergeSelection); } } From 0833a6fb9a3dec55c464f211340f1d51a1bbdb97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 15:38:13 +0900 Subject: [PATCH 173/532] Avoid multiple iteration of LINQ query using a local --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 663bf03a00..f3c0a05bc2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -348,13 +348,15 @@ namespace osu.Game.Rulesets.Osu.Edit private void mergeSelection() { - if (selectedMergeableObjects.Length < 2) + var mergeableObjects = selectedMergeableObjects; + + if (mergeableObjects.Length < 2) return; ChangeHandler?.BeginChange(); // Have an initial slider object. - var firstHitObject = selectedMergeableObjects[0]; + var firstHitObject = mergeableObjects[0]; var mergedHitObject = firstHitObject as Slider ?? new Slider { StartTime = firstHitObject.StartTime, @@ -371,7 +373,7 @@ namespace osu.Game.Rulesets.Osu.Edit // Merge all the selected hit objects into one slider path. bool lastCircle = firstHitObject is HitCircle; - foreach (var selectedMergeableObject in selectedMergeableObjects.Skip(1)) + foreach (var selectedMergeableObject in mergeableObjects.Skip(1)) { if (selectedMergeableObject is IHasPath hasPath) { @@ -407,14 +409,14 @@ namespace osu.Game.Rulesets.Osu.Edit // Make sure only the merged hit object is in the beatmap. if (firstHitObject is Slider) { - foreach (var selectedMergeableObject in selectedMergeableObjects.Skip(1)) + foreach (var selectedMergeableObject in mergeableObjects.Skip(1)) { EditorBeatmap.Remove(selectedMergeableObject); } } else { - foreach (var selectedMergeableObject in selectedMergeableObjects) + foreach (var selectedMergeableObject in mergeableObjects) { EditorBeatmap.Remove(selectedMergeableObject); } From 11f38e539f6571ff1811db64883a219e2930f5c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 16:01:19 +0900 Subject: [PATCH 174/532] Rename property to `LastLocalUpdate` --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Database/RealmAccess.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 29f04e5a54..32b7f0b29b 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -105,7 +105,7 @@ namespace osu.Game.Beatmaps /// /// The last time of a local modification (via the editor). /// - public DateTimeOffset? LastUpdated { get; set; } + public DateTimeOffset? LastLocalUpdate { get; set; } /// /// The last time online metadata was applied to this beatmap. diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d6ef1e0173..d736765dd9 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -314,7 +314,7 @@ namespace osu.Game.Beatmaps beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.Hash = stream.ComputeSHA2Hash(); - beatmapInfo.LastUpdated = DateTimeOffset.Now; + beatmapInfo.LastLocalUpdate = DateTimeOffset.Now; beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo)); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 40ac3b7422..cdaf35a1fb 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -68,7 +68,7 @@ namespace osu.Game.Database /// 20 2022-07-21 Added LastAppliedDifficultyVersion to RulesetInfo, changed default value of BeatmapInfo.StarRating to -1. /// 21 2022-07-27 Migrate collections to realm (BeatmapCollection). /// 22 2022-07-31 Added ModPreset. - /// 23 2022-08-01 Added LastUpdated to BeatmapInfo. + /// 23 2022-08-01 Added LastLocalUpdate to BeatmapInfo. /// private const int schema_version = 23; From 9d2c2b71cf07a172a0f83e246e79e6b06822e29a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 16:21:35 +0900 Subject: [PATCH 175/532] Change conditional to check for insertions in addition to modifications It is possible that the import process itself marks the previous beatmaps as deleted due to an overlap in metadata or otherwise. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index e9419e7156..e9f676d32f 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -273,11 +273,13 @@ namespace osu.Game.Screens.Select // Check if the current selection was potentially deleted by re-querying its validity. bool selectedSetMarkedDeleted = realm.Run(r => r.Find(SelectedBeatmapSet.ID))?.DeletePending != false; - if (selectedSetMarkedDeleted && changes.NewModifiedIndices.Any()) + int[] modifiedAndInserted = changes.NewModifiedIndices.Concat(changes.InsertedIndices).ToArray(); + + if (selectedSetMarkedDeleted && modifiedAndInserted.Any()) { // If it is no longer valid, make the bold assumption that an updated version will be available in the modified indices. // This relies on the full update operation being in a single transaction, so please don't change that. - foreach (int i in changes.NewModifiedIndices) + foreach (int i in modifiedAndInserted) { var beatmapSetInfo = sender[i]; From ee153a345c34c825852883723a8339a2593c062a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 16:31:56 +0900 Subject: [PATCH 176/532] Add a few more overlooked beatmap save states on setup screen modifications --- osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs | 1 + osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs | 1 + osu.Game/Screens/Edit/Setup/DifficultySection.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs index b602a67baa..81c41ab628 100644 --- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs +++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaSetupSection.cs @@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup private void updateBeatmap() { Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value; + Beatmap.SaveState(); } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs b/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs index d8471aca1a..2889832ff1 100644 --- a/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs +++ b/osu.Game.Rulesets.Osu/Edit/Setup/OsuSetupSection.cs @@ -49,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup private void updateBeatmap() { Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value; + Beatmap.SaveState(); } } } diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index 5ce5d05d64..ce44445683 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -96,6 +96,7 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; Beatmap.UpdateAllHitObjects(); + Beatmap.SaveState(); } } } From 1a7ddc004075c0ebb9774a64080314dfe1a3e7a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 16:43:05 +0900 Subject: [PATCH 177/532] Fix re-importing existing collections not correctly adding new items --- osu.Game/Database/LegacyCollectionImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/LegacyCollectionImporter.cs b/osu.Game/Database/LegacyCollectionImporter.cs index 4bb28bf731..6d3e3fb76a 100644 --- a/osu.Game/Database/LegacyCollectionImporter.cs +++ b/osu.Game/Database/LegacyCollectionImporter.cs @@ -89,7 +89,7 @@ namespace osu.Game.Database if (existing != null) { - foreach (string newBeatmap in existing.BeatmapMD5Hashes) + foreach (string newBeatmap in collection.BeatmapMD5Hashes) { if (!existing.BeatmapMD5Hashes.Contains(newBeatmap)) existing.BeatmapMD5Hashes.Add(newBeatmap); From 5ac314077a706610c86ff9e3926fe57f0d46df10 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Aug 2022 18:09:04 +0900 Subject: [PATCH 178/532] Improve intro timings when not using osu! theme Roughly as proposed in https://github.com/ppy/osu/discussions/19687. --- osu.Game/Screens/Menu/IntroCircles.cs | 11 ++++++++--- osu.Game/Screens/Menu/IntroScreen.cs | 14 ++++++++++---- osu.Game/Screens/Menu/IntroTriangles.cs | 15 +++++++++------ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroCircles.cs b/osu.Game/Screens/Menu/IntroCircles.cs index faa9a267ce..5f481ed00e 100644 --- a/osu.Game/Screens/Menu/IntroCircles.cs +++ b/osu.Game/Screens/Menu/IntroCircles.cs @@ -19,8 +19,11 @@ namespace osu.Game.Screens.Menu protected override string BeatmapFile => "circles.osz"; + public const double TRACK_START_DELAY_NON_THEMED = 1000; + private const double track_start_delay_themed = 600; + + private const double delay_for_menu = 2900; private const double delay_step_one = 2300; - private const double delay_step_two = 600; private Sample welcome; @@ -44,14 +47,16 @@ namespace osu.Game.Screens.Menu { welcome?.Play(); + double trackStartDelay = UsingThemedIntro ? track_start_delay_themed : TRACK_START_DELAY_NON_THEMED; + Scheduler.AddDelayed(delegate { StartTrack(); PrepareMenuLoad(); - Scheduler.AddDelayed(LoadMenu, delay_step_one); - }, delay_step_two); + Scheduler.AddDelayed(LoadMenu, delay_for_menu - trackStartDelay); + }, trackStartDelay); logo.ScaleTo(1); logo.FadeIn(); diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index a2ecd7eacb..bf713997f7 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -272,11 +272,17 @@ namespace osu.Game.Screens.Menu FadeInBackground(200); } - protected virtual void StartTrack() + protected void StartTrack() { - // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Menu. - if (UsingThemedIntro) - Track.Start(); + var drawableTrack = musicController.CurrentTrack; + + drawableTrack.Start(); + + if (!UsingThemedIntro) + { + drawableTrack.VolumeTo(0).Then() + .VolumeTo(1, 2000, Easing.OutQuint); + } } protected override void LogoArriving(OsuLogo logo, bool resuming) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 6ad0350e43..4ec79b852a 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -84,9 +84,17 @@ namespace osu.Game.Screens.Menu return; if (!UsingThemedIntro) + { + // If the user has requested no theme, fallback to the same intro voice and delay as IntroCircles. + // The triangles intro voice and theme are combined which makes it impossible to use. welcome?.Play(); + Scheduler.AddDelayed(StartTrack, IntroCircles.TRACK_START_DELAY_NON_THEMED); + } + else + StartTrack(); - StartTrack(); + // no-op for the case of themed intro, no harm in calling for both scenarios as a safety measure. + decoupledClock.Start(); }); } } @@ -99,11 +107,6 @@ namespace osu.Game.Screens.Menu intro.Expire(); } - protected override void StartTrack() - { - decoupledClock.Start(); - } - private class TrianglesIntroSequence : CompositeDrawable { private readonly OsuLogo logo; From 43e471c2a5832621ca91a4eea98650511a678bff Mon Sep 17 00:00:00 2001 From: StanR Date: Tue, 16 Aug 2022 16:12:13 +0300 Subject: [PATCH 179/532] Clamp effective miss count to maximum amount of possible braks --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 3c82c2dc33..fb0eff5cb2 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -270,8 +270,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); } - // Clamp miss count since it's derived from combo and can be higher than total hits and that breaks some calculations - comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits); + // Clamp miss count to maximum amount of possible breaks + comboBasedMissCount = Math.Min(comboBasedMissCount, countOk + countMeh + countMiss); return Math.Max(countMiss, comboBasedMissCount); } From 6bfdfeb15356941abc40ec1e1b5cdd4e293b04cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Aug 2022 22:41:32 +0200 Subject: [PATCH 180/532] Refactor mod panel selection logic to avoid overwriting --- osu.Game/Overlays/Mods/ModPanel.cs | 22 +++++++++------------- osu.Game/Overlays/Mods/ModPresetPanel.cs | 16 +++++++++------- osu.Game/Overlays/Mods/ModSelectPanel.cs | 18 +++++++++++++++++- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 0ab6293faf..8fc7d5a738 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -37,8 +37,6 @@ namespace osu.Game.Overlays.Mods Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE) }; - - Action = select; } public ModPanel(Mod mod) @@ -59,18 +57,16 @@ namespace osu.Game.Overlays.Mods Filtered.BindValueChanged(_ => updateFilterState(), true); } - private void select() + protected override void Select() { - if (!Active.Value) - { - modState.RequiresConfiguration = Mod.RequiresConfiguration; - Active.Value = true; - } - else - { - modState.RequiresConfiguration = false; - Active.Value = false; - } + modState.RequiresConfiguration = Mod.RequiresConfiguration; + Active.Value = true; + } + + protected override void Deselect() + { + modState.RequiresConfiguration = false; + Active.Value = false; } #region Filtering support diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index a259645479..b314a19142 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -37,8 +37,6 @@ namespace osu.Game.Overlays.Mods Title = preset.Value.Name; Description = preset.Value.Description; - - Action = toggleRequestedByUser; } [BackgroundDependencyLoader] @@ -54,15 +52,19 @@ namespace osu.Game.Overlays.Mods selectedMods.BindValueChanged(_ => selectedModsChanged(), true); } - private void toggleRequestedByUser() + protected override void Select() + { + // if the preset is not active at the point of the user click, then set the mods using the preset directly, discarding any previous selections, + // which will also have the side effect of activating the preset (see `updateActiveState()`). + selectedMods.Value = Preset.Value.Mods.ToArray(); + } + + protected override void Deselect() { - // if the preset is not active at the point of the user click, then set the mods using the preset directly, discarding any previous selections. // if the preset is active when the user has clicked it, then it means that the set of active mods is exactly equal to the set of mods in the preset // (there are no other active mods than what the preset specifies, and the mod settings match exactly). // therefore it's safe to just clear selected mods, since it will have the effect of toggling the preset off. - selectedMods.Value = !Active.Value - ? Preset.Value.Mods.ToArray() - : Array.Empty(); + selectedMods.Value = Array.Empty(); } private void selectedModsChanged() diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index b3df00f8f9..27abface0c 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -143,9 +143,25 @@ namespace osu.Game.Overlays.Mods } }; - Action = () => Active.Toggle(); + Action = () => + { + if (!Active.Value) + Select(); + else + Deselect(); + }; } + /// + /// Performs all actions necessary to select this . + /// + protected abstract void Select(); + + /// + /// Performs all actions necessary to deselect this . + /// + protected abstract void Deselect(); + [BackgroundDependencyLoader] private void load(AudioManager audio, ISamplePlaybackDisabler? samplePlaybackDisabler) { From 3109066e340d01f24b8c6c6e9b8787b35c0c2fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Aug 2022 22:42:30 +0200 Subject: [PATCH 181/532] Rename `{Requires -> Pending}Configuration` --- osu.Game/Overlays/Mods/ModPanel.cs | 4 ++-- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 8 ++++---- osu.Game/Overlays/Mods/ModState.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 8fc7d5a738..7bdb9511ac 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -59,13 +59,13 @@ namespace osu.Game.Overlays.Mods protected override void Select() { - modState.RequiresConfiguration = Mod.RequiresConfiguration; + modState.PendingConfiguration = Mod.RequiresConfiguration; Active.Value = true; } protected override void Deselect() { - modState.RequiresConfiguration = false; + modState.PendingConfiguration = false; Active.Value = false; } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 66ecb9fd78..b993aca0ca 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -362,20 +362,20 @@ namespace osu.Game.Overlays.Mods return; bool anyCustomisableModActive = false; - bool anyModRequiresCustomisation = false; + bool anyModPendingConfiguration = false; foreach (var modState in allAvailableMods) { anyCustomisableModActive |= modState.Active.Value && modState.Mod.GetSettingsSourceProperties().Any(); - anyModRequiresCustomisation |= modState.RequiresConfiguration; - modState.RequiresConfiguration = false; + anyModPendingConfiguration |= modState.PendingConfiguration; + modState.PendingConfiguration = false; } if (anyCustomisableModActive) { customisationVisible.Disabled = false; - if (anyModRequiresCustomisation && !customisationVisible.Value) + if (anyModPendingConfiguration && !customisationVisible.Value) customisationVisible.Value = true; } else diff --git a/osu.Game/Overlays/Mods/ModState.cs b/osu.Game/Overlays/Mods/ModState.cs index a3806b1936..3ee890e876 100644 --- a/osu.Game/Overlays/Mods/ModState.cs +++ b/osu.Game/Overlays/Mods/ModState.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Mods /// This flag is read by the to determine if the customisation panel should be opened after a mod change /// and cleared after reading. /// - public bool RequiresConfiguration { get; set; } + public bool PendingConfiguration { get; set; } /// /// Whether the mod is currently filtered out due to not matching imposed criteria. From d021218d78cf102b45bf52d3670e5be9fc507a64 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 16 Aug 2022 23:05:35 +0200 Subject: [PATCH 182/532] added test for objects which cant be merged --- .../Editor/TestSceneObjectMerging.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 63e6760514..b68231ce64 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -141,6 +141,33 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); } + [Test] + public void TestNonMerge() + { + HitCircle? circle1 = null; + HitCircle? circle2 = null; + Spinner? spinner = null; + + AddStep("select first two circles and spinner", () => + { + circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle); + circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1); + spinner = (Spinner)EditorBeatmap.HitObjects.First(h => h is Spinner); + EditorClock.Seek(spinner.StartTime); + EditorBeatmap.SelectedHitObjects.Add(circle1); + EditorBeatmap.SelectedHitObjects.Add(circle2); + EditorBeatmap.SelectedHitObjects.Add(spinner); + }); + + mergeSelection(); + + AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( + (pos: circle1.Position, pathType: PathType.Linear), + (pos: circle2.Position, pathType: null))); + + AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner)); + } + private void mergeSelection() { AddStep("merge selection", () => From 37799e3b31edc2fe4b71ce8308fbceecb2ec8646 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 13:20:24 +0900 Subject: [PATCH 183/532] Allow preparing preview point without looping --- osu.Game/Beatmaps/IWorkingBeatmap.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 4 ++-- osu.Game/Screens/Menu/MainMenu.cs | 2 +- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 548341cc77..a39766abe1 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -134,6 +134,6 @@ namespace osu.Game.Beatmaps /// /// Reads the correct track restart point from beatmap metadata and sets looping to enabled. /// - void PrepareTrackForPreviewLooping(); + void PrepareTrackForPreview(bool looping); } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 16464932e0..301610ee58 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -110,9 +110,9 @@ namespace osu.Game.Beatmaps public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000); - public void PrepareTrackForPreviewLooping() + public void PrepareTrackForPreview(bool looping) { - Track.Looping = true; + Track.Looping = looping; Track.RestartPoint = Metadata.PreviewTime; if (Track.RestartPoint == -1) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 066a37055c..0071ada05a 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -192,7 +192,7 @@ namespace osu.Game.Screens.Menu // presume the track is the current beatmap's track. not sure how correct this assumption is but it has worked until now. if (!track.IsRunning) { - Beatmap.Value.PrepareTrackForPreviewLooping(); + Beatmap.Value.PrepareTrackForPreview(false); track.Restart(); } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 25f2a94a3c..03216180fb 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -485,7 +485,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (track != null) { - Beatmap.Value.PrepareTrackForPreviewLooping(); + Beatmap.Value.PrepareTrackForPreview(true); music?.EnsurePlayingSomething(); } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 33ff31857f..0c2ca6d4af 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -683,7 +683,7 @@ namespace osu.Game.Screens.Select } private void ensureTrackLooping(IWorkingBeatmap beatmap, TrackChangeDirection changeDirection) - => beatmap.PrepareTrackForPreviewLooping(); + => beatmap.PrepareTrackForPreview(true); public override bool OnBackButton() { From d9346abb9c3b390357d060441fc7020915573c9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 13:20:40 +0900 Subject: [PATCH 184/532] Tweak intro timings more and ensure non-theme tracks play from preview point --- osu.Game/Screens/Menu/IntroCircles.cs | 10 +++------- osu.Game/Screens/Menu/IntroScreen.cs | 14 ++++++++++---- osu.Game/Screens/Menu/IntroTriangles.cs | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroCircles.cs b/osu.Game/Screens/Menu/IntroCircles.cs index 5f481ed00e..7a4bdb231f 100644 --- a/osu.Game/Screens/Menu/IntroCircles.cs +++ b/osu.Game/Screens/Menu/IntroCircles.cs @@ -19,11 +19,9 @@ namespace osu.Game.Screens.Menu protected override string BeatmapFile => "circles.osz"; - public const double TRACK_START_DELAY_NON_THEMED = 1000; - private const double track_start_delay_themed = 600; + public const double TRACK_START_DELAY = 600; private const double delay_for_menu = 2900; - private const double delay_step_one = 2300; private Sample welcome; @@ -47,16 +45,14 @@ namespace osu.Game.Screens.Menu { welcome?.Play(); - double trackStartDelay = UsingThemedIntro ? track_start_delay_themed : TRACK_START_DELAY_NON_THEMED; - Scheduler.AddDelayed(delegate { StartTrack(); PrepareMenuLoad(); - Scheduler.AddDelayed(LoadMenu, delay_for_menu - trackStartDelay); - }, trackStartDelay); + Scheduler.AddDelayed(LoadMenu, delay_for_menu - TRACK_START_DELAY); + }, TRACK_START_DELAY); logo.ScaleTo(1); logo.FadeIn(); diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index bf713997f7..bd49eba663 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -276,12 +276,18 @@ namespace osu.Game.Screens.Menu { var drawableTrack = musicController.CurrentTrack; - drawableTrack.Start(); - if (!UsingThemedIntro) { - drawableTrack.VolumeTo(0).Then() - .VolumeTo(1, 2000, Easing.OutQuint); + initialBeatmap.PrepareTrackForPreview(false); + + drawableTrack.VolumeTo(0); + drawableTrack.Restart(); + drawableTrack.VolumeTo(1, 2200, Easing.InCubic); + } + else + + { + drawableTrack.Restart(); } } diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 4ec79b852a..d777f78df2 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -88,7 +88,7 @@ namespace osu.Game.Screens.Menu // If the user has requested no theme, fallback to the same intro voice and delay as IntroCircles. // The triangles intro voice and theme are combined which makes it impossible to use. welcome?.Play(); - Scheduler.AddDelayed(StartTrack, IntroCircles.TRACK_START_DELAY_NON_THEMED); + Scheduler.AddDelayed(StartTrack, IntroCircles.TRACK_START_DELAY); } else StartTrack(); From 0ff5547b8356fe9e06f1c3dc1325f2073bdfd608 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 13:35:44 +0900 Subject: [PATCH 185/532] Make `changeHandler` optional in `convertToStream` method to match new implementation --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index bd11cc826f..794551dab7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -251,13 +251,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void convertToStream() { - if (editorBeatmap == null || changeHandler == null || beatDivisor == null) + if (editorBeatmap == null || beatDivisor == null) return; var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime); double streamSpacing = timingPoint.BeatLength / beatDivisor.Value; - changeHandler.BeginChange(); + changeHandler?.BeginChange(); int i = 0; double time = HitObject.StartTime; @@ -292,7 +292,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders editorBeatmap.Remove(HitObject); - changeHandler.EndChange(); + changeHandler?.EndChange(); } public override MenuItem[] ContextMenuItems => new MenuItem[] From 8b5ac55fca7b2cb3a5bf6a826de7f461aee14b3e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 17 Aug 2022 13:48:06 +0900 Subject: [PATCH 186/532] Remove newline --- osu.Game/Screens/Menu/IntroScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index bd49eba663..a07ebb463a 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -285,7 +285,6 @@ namespace osu.Game.Screens.Menu drawableTrack.VolumeTo(1, 2200, Easing.InCubic); } else - { drawableTrack.Restart(); } From 8ce50e98a6ef52132350f77f186d9c01dec2b681 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 14:04:55 +0900 Subject: [PATCH 187/532] Move delegate debounce logic to `Editor` itself --- osu.Game/Screens/Edit/Editor.cs | 21 ++++++++++++++++++++- osu.Game/Screens/Edit/EditorBeatmap.cs | 24 ++++++++---------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 90e912bd6d..9cf0a0fc0c 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -24,6 +24,7 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Framework.Threading; using osu.Framework.Timing; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -231,6 +232,8 @@ namespace osu.Game.Screens.Edit AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin(), loadableBeatmap.BeatmapInfo)); dependencies.CacheAs(editorBeatmap); + editorBeatmap.UpdateInProgress.BindValueChanged(updateInProgress); + canSave = editorBeatmap.BeatmapInfo.Ruleset.CreateInstance() is ILegacyRuleset; if (canSave) @@ -431,7 +434,7 @@ namespace osu.Game.Screens.Edit samplePlaybackDisabled.Value = clock.SeekingOrStopped.Value || currentScreen is not ComposeScreen - || editorBeatmap.UpdateInProgress; + || temporaryMuteFromUpdateInProgress; } public bool OnPressed(KeyBindingPressEvent e) @@ -717,6 +720,22 @@ namespace osu.Game.Screens.Edit this.Exit(); } + #region Mute from update application + + private ScheduledDelegate temporaryMuteRestorationDelegate; + private bool temporaryMuteFromUpdateInProgress; + + private void updateInProgress(ValueChangedEvent obj) + { + temporaryMuteFromUpdateInProgress = true; + + // Debounce is arbitrarily high enough to avoid flip-flopping the value each other frame. + temporaryMuteRestorationDelegate?.Cancel(); + temporaryMuteRestorationDelegate = Scheduler.AddDelayed(() => temporaryMuteFromUpdateInProgress = false, 50); + } + + #endregion + #region Clipboard support private EditorMenuItem cutMenuItem; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index b9b0879fa4..6154485bc2 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -10,7 +10,6 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; @@ -24,13 +23,15 @@ namespace osu.Game.Screens.Edit public class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBeatSnapProvider { /// - /// While performing updates on hitobjects, this will momentarily become true. + /// Will become true when a new update is queued, and false when all updates have been applied. /// /// /// This is intended to be used to avoid performing operations (like playback of samples) /// while mutating hitobjects. /// - public bool UpdateInProgress { get; private set; } + public IBindable UpdateInProgress => updateInProgress; + + private readonly BindableBool updateInProgress = new BindableBool(); /// /// Invoked when a is added to this . @@ -236,11 +237,9 @@ namespace osu.Game.Screens.Edit // updates are debounced regardless of whether a batch is active. batchPendingUpdates.Add(hitObject); - advertiseUpdateInProgress(); + updateInProgress.Value = true; } - private ScheduledDelegate updateCompleteDelegate; - /// /// Update all hit objects with potentially changed difficulty or control point data. /// @@ -249,7 +248,7 @@ namespace osu.Game.Screens.Edit foreach (var h in HitObjects) batchPendingUpdates.Add(h); - advertiseUpdateInProgress(); + updateInProgress.Value = true; } /// @@ -342,6 +341,8 @@ namespace osu.Game.Screens.Edit foreach (var h in deletes) HitObjectRemoved?.Invoke(h); foreach (var h in inserts) HitObjectAdded?.Invoke(h); foreach (var h in updates) HitObjectUpdated?.Invoke(h); + + updateInProgress.Value = false; } /// @@ -349,15 +350,6 @@ namespace osu.Game.Screens.Edit /// public void Clear() => RemoveRange(HitObjects.ToArray()); - private void advertiseUpdateInProgress() - { - UpdateInProgress = true; - - // Debounce is arbitrarily high enough to avoid flip-flopping the value each other frame. - updateCompleteDelegate?.Cancel(); - updateCompleteDelegate = Scheduler.AddDelayed(() => UpdateInProgress = false, 50); - } - private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, PlayableBeatmap.Difficulty); private void trackStartTime(HitObject hitObject) From 6b9dec599698d8ee371837eb47b84e6b182d165a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 14:32:21 +0900 Subject: [PATCH 188/532] Restore original event flow to allow for `OnSuspend` case to work correctly --- osu.Game/Screens/Edit/Editor.cs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 9cf0a0fc0c..a7cbe1f1ad 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -226,6 +226,8 @@ namespace osu.Game.Screens.Edit dependencies.CacheAs(clock); AddInternal(clock); + clock.SeekingOrStopped.BindValueChanged(_ => updateSampleDisabledState()); + // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); @@ -429,12 +431,7 @@ namespace osu.Game.Screens.Edit protected override void Update() { base.Update(); - clock.ProcessFrame(); - - samplePlaybackDisabled.Value = clock.SeekingOrStopped.Value - || currentScreen is not ComposeScreen - || temporaryMuteFromUpdateInProgress; } public bool OnPressed(KeyBindingPressEvent e) @@ -728,10 +725,15 @@ namespace osu.Game.Screens.Edit private void updateInProgress(ValueChangedEvent obj) { temporaryMuteFromUpdateInProgress = true; + updateSampleDisabledState(); // Debounce is arbitrarily high enough to avoid flip-flopping the value each other frame. temporaryMuteRestorationDelegate?.Cancel(); - temporaryMuteRestorationDelegate = Scheduler.AddDelayed(() => temporaryMuteFromUpdateInProgress = false, 50); + temporaryMuteRestorationDelegate = Scheduler.AddDelayed(() => + { + temporaryMuteFromUpdateInProgress = false; + updateSampleDisabledState(); + }, 50); } #endregion @@ -844,10 +846,18 @@ namespace osu.Game.Screens.Edit } finally { + updateSampleDisabledState(); rebindClipboardBindables(); } } + private void updateSampleDisabledState() + { + samplePlaybackDisabled.Value = clock.SeekingOrStopped.Value + || currentScreen is not ComposeScreen + || temporaryMuteFromUpdateInProgress; + } + private void seek(UIEvent e, int direction) { double amount = e.ShiftPressed ? 4 : 1; @@ -952,7 +962,11 @@ namespace osu.Game.Screens.Edit protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset)); - private void cancelExit() => loader?.CancelPendingDifficultySwitch(); + private void cancelExit() + { + updateSampleDisabledState(); + loader?.CancelPendingDifficultySwitch(); + } public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); From 21b364cd7758f9574b94f3bdd97970e905c6bd18 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 14:48:00 +0900 Subject: [PATCH 189/532] Fix nullref in tests as `initialBeatmap` may be null --- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index a07ebb463a..409c7d6c8d 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -278,7 +278,7 @@ namespace osu.Game.Screens.Menu if (!UsingThemedIntro) { - initialBeatmap.PrepareTrackForPreview(false); + initialBeatmap?.PrepareTrackForPreview(false); drawableTrack.VolumeTo(0); drawableTrack.Restart(); From 7191fbb6d615c327b3a67dfe3a871fa5d0ba8fbd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 16:40:07 +0900 Subject: [PATCH 190/532] 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 247140ceef..74e5b49167 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c17d45e84a..c3c9834724 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 5bfb53bc9d..da48550efb 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - + From a5ac69a5549a11c4d12f2614a7afe589e438a136 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 16:42:34 +0900 Subject: [PATCH 191/532] Update various dependencies --- .../osu.Game.Rulesets.Osu.Tests.csproj | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- osu.Game/osu.Game.csproj | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index 4349d25cb3..36c40c0fe2 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 7615b3e8be..65679deb01 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -7,7 +7,7 @@ - + WinExe diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c3c9834724..d67f8415e7 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,9 +23,9 @@ - - - + + + @@ -35,13 +35,13 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + - + From 3a0017c87b31a05b3b872f35793027356cb4e063 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 17 Aug 2022 17:07:06 +0900 Subject: [PATCH 192/532] Fix flaky quick retry test --- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index a38089e023..b0d7eadaa7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -432,7 +432,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("restart completed", () => getCurrentPlayer() != null && getCurrentPlayer() != previousPlayer); AddStep("release quick retry key", () => InputManager.ReleaseKey(Key.Tilde)); - AddUntilStep("wait for load", () => getCurrentPlayer()?.LoadedBeatmapSuccessfully == true); + AddUntilStep("wait for player", () => getCurrentPlayer()?.LoadState == LoadState.Ready); AddUntilStep("time reached zero", () => getCurrentPlayer()?.GameplayClockContainer.CurrentTime > 0); AddUntilStep("skip button not visible", () => !checkSkipButtonVisible()); From 243fe56b1dc13987b12203537db12ab9a559ff3a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 17:11:17 +0900 Subject: [PATCH 193/532] Realm bump for props --- osu.Android.props | 2 +- osu.iOS.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 74e5b49167..6dbc6cc377 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -56,6 +56,6 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index da48550efb..463af1143f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -89,6 +89,6 @@ - + From fca076b9886f8552797e24b23c9f52603c718e5f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 17:17:22 +0900 Subject: [PATCH 194/532] Fix edge case of realm backup cascading failure --- osu.Game/Database/RealmAccess.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index cdaf35a1fb..dd219cc1aa 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -902,7 +902,14 @@ namespace osu.Game.Database { using (var source = storage.GetStream(Filename, mode: FileMode.Open)) using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew)) + { + // source may not exist. + if (source == null) + return; + source.CopyTo(destination); + } + return; } catch (IOException) From e1e6be039a234a3d0ba2cd44588a0cd0881dac0f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 18:20:47 +0900 Subject: [PATCH 195/532] Don't create destination stream if backup source doesn't exist --- osu.Game/Database/RealmAccess.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index dd219cc1aa..0f2e724567 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -901,13 +901,13 @@ namespace osu.Game.Database try { using (var source = storage.GetStream(Filename, mode: FileMode.Open)) - using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew)) { // source may not exist. if (source == null) return; - source.CopyTo(destination); + using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew)) + source.CopyTo(destination); } return; From 203b8b22b90216a26a02ae69294d52a41a248711 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 17 Aug 2022 19:02:13 +0900 Subject: [PATCH 196/532] Adjust tests --- .../ManiaDifficultyCalculatorTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs index 9e4db1f1c9..4ae6cb9c7c 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs @@ -16,11 +16,11 @@ namespace osu.Game.Rulesets.Mania.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; - [TestCase(2.3449735700206298d, 242, "diffcalc-test")] + [TestCase(2.3493769750220914d, 242, "diffcalc-test")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(2.7879104989252959d, 242, "diffcalc-test")] + [TestCase(2.797245912537965d, 242, "diffcalc-test")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime()); From f381bc91159db110cd5535bf46d2d0d878e93822 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 17 Aug 2022 19:03:48 +0900 Subject: [PATCH 197/532] Add explanatory comment --- osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index d5cee47178..2c7c84de97 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -89,6 +89,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills startTimes[column] = startTime; endTimes[column] = endTime; + // By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section. return individualStrain + overallStrain - CurrentStrain; } From 4ef4d66f491e6717ca9c3aece472e2b820e0563f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 16:39:24 +0900 Subject: [PATCH 198/532] Add some extra initial state checks to `TestSceneEditorSeekSnapping` --- osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs index d24baa6f63..2465512dae 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs @@ -173,6 +173,7 @@ namespace osu.Game.Tests.Visual.Editing reset(); AddStep("Seek(49)", () => Clock.Seek(49)); + checkTime(49); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); checkTime(50); AddStep("Seek(49.999)", () => Clock.Seek(49.999)); @@ -207,6 +208,7 @@ namespace osu.Game.Tests.Visual.Editing reset(); AddStep("Seek(450)", () => Clock.Seek(450)); + checkTime(450); AddStep("SeekBackward", () => Clock.SeekBackward()); checkTime(400); AddStep("SeekBackward", () => Clock.SeekBackward()); @@ -228,6 +230,7 @@ namespace osu.Game.Tests.Visual.Editing reset(); AddStep("Seek(450)", () => Clock.Seek(450)); + checkTime(450); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); checkTime(400); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); @@ -252,6 +255,7 @@ namespace osu.Game.Tests.Visual.Editing reset(); AddStep("Seek(451)", () => Clock.Seek(451)); + checkTime(451); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); checkTime(450); AddStep("Seek(450.999)", () => Clock.Seek(450.999)); @@ -276,6 +280,7 @@ namespace osu.Game.Tests.Visual.Editing double lastTime = 0; AddStep("Seek(0)", () => Clock.Seek(0)); + checkTime(0); for (int i = 0; i < 9; i++) { From 7d8fbc4dbc3408204dcd41f63063ed41642e702f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 17:36:41 +0900 Subject: [PATCH 199/532] Refactor `TestSceneDrawableTaikoMascot` to read a bit better --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index ef95358d34..7dadac85bb 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using Humanizer; @@ -36,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning TimeRange = { Value = 5000 }, }; - private TaikoScoreProcessor scoreProcessor; + private TaikoScoreProcessor scoreProcessor = null!; private IEnumerable mascots => this.ChildrenOfType(); @@ -89,9 +87,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [Test] public void TestIdleState() { - AddStep("set beatmap", () => setBeatmap()); - - createDrawableRuleset(); + prepareTest(false); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle); @@ -100,9 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [Test] public void TestKiaiState() { - AddStep("set beatmap", () => setBeatmap(true)); - - createDrawableRuleset(); + prepareTest(true); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Kiai); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Kiai); @@ -112,9 +106,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [Test] public void TestMissState() { - AddStep("set beatmap", () => setBeatmap()); - - createDrawableRuleset(); + prepareTest(false); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail); @@ -128,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { AddStep("set beatmap", () => setBeatmap(kiai)); - createDrawableRuleset(); + prepareTest(kiai); AddRepeatStep("reach 49 combo", () => applyNewResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }), 49); @@ -139,9 +131,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [TestCase(false, TaikoMascotAnimationState.Idle)] public void TestClearStateOnClearedSwell(bool kiai, TaikoMascotAnimationState expectedStateAfterClear) { - AddStep("set beatmap", () => setBeatmap(kiai)); - - createDrawableRuleset(); + prepareTest(kiai); assertStateAfterResult(new JudgementResult(new Swell(), new TaikoSwellJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Clear); AddUntilStep($"state reverts to {expectedStateAfterClear.ToString().ToLowerInvariant()}", () => allMascotsIn(expectedStateAfterClear)); @@ -175,25 +165,27 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning scoreProcessor.ApplyBeatmap(Beatmap.Value.Beatmap); } - private void createDrawableRuleset() + private void prepareTest(bool kiai) { - AddUntilStep("wait for beatmap to be loaded", () => Beatmap.Value.Track.IsLoaded); + AddStep("set beatmap", () => setBeatmap(kiai)); AddStep("create drawable ruleset", () => { - Beatmap.Value.Track.Start(); - SetContents(_ => { var ruleset = new TaikoRuleset(); return new DrawableTaikoRuleset(ruleset, Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); }); }); + + AddUntilStep("wait for track to be loaded", () => MusicController.TrackLoaded); + AddStep("start track", () => MusicController.CurrentTrack.Restart()); + AddUntilStep("wait for track started", () => MusicController.IsPlaying); } private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState) { - TaikoMascotAnimationState[] mascotStates = null; + TaikoMascotAnimationState[] mascotStates = null!; AddStep($"{judgementResult.Type.ToString().ToLowerInvariant()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}", () => @@ -204,7 +196,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning Schedule(() => mascotStates = animatedMascots.Select(mascot => mascot.State.Value).ToArray()); }); - AddAssert($"state is {expectedState.ToString().ToLowerInvariant()}", () => mascotStates.All(state => state == expectedState)); + AddAssert($"state is {expectedState.ToString().ToLowerInvariant()}", () => mascotStates.Distinct(), () => Is.EquivalentTo(new[] { expectedState })); } private void applyNewResult(JudgementResult judgementResult) From 553ae4781f0eb3dc6ca26c3def0ce1c43dd87d01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 18:51:57 +0900 Subject: [PATCH 200/532] Remove unnecessary local implementation in `TestScenePlaybackControl` --- .../Visual/Editing/TestScenePlaybackControl.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs index 78f650f0fa..674476d644 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs @@ -1,13 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Beatmaps; -using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; using osuTK; @@ -19,19 +15,12 @@ namespace osu.Game.Tests.Visual.Editing [BackgroundDependencyLoader] private void load() { - var clock = new EditorClock { IsCoupled = false }; - Dependencies.CacheAs(clock); - - var playback = new PlaybackControl + Child = new PlaybackControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(200, 100) }; - - Beatmap.Value = CreateWorkingBeatmap(new Beatmap()); - - Child = playback; } } } From d40d09a5442159c249e8cec887d004b5ca53f1b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 23:03:39 +0900 Subject: [PATCH 201/532] Rename method to be more specific and standardise `setBeatmap` calls --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 7dadac85bb..9163f994c5 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -63,6 +63,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [Test] public void TestInitialState() { + AddStep("set beatmap", () => setBeatmap()); + AddStep("create mascot", () => SetContents(_ => new DrawableTaikoMascot { RelativeSizeAxes = Axes.Both })); AddAssert("mascot initially idle", () => allMascotsIn(TaikoMascotAnimationState.Idle)); @@ -87,7 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [Test] public void TestIdleState() { - prepareTest(false); + prepareDrawableRulesetAndBeatmap(false); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle); @@ -96,7 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [Test] public void TestKiaiState() { - prepareTest(true); + prepareDrawableRulesetAndBeatmap(true); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Kiai); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Kiai); @@ -106,7 +108,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [Test] public void TestMissState() { - prepareTest(false); + prepareDrawableRulesetAndBeatmap(false); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail); @@ -118,9 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [TestCase(false)] public void TestClearStateOnComboMilestone(bool kiai) { - AddStep("set beatmap", () => setBeatmap(kiai)); - - prepareTest(kiai); + prepareDrawableRulesetAndBeatmap(kiai); AddRepeatStep("reach 49 combo", () => applyNewResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }), 49); @@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [TestCase(false, TaikoMascotAnimationState.Idle)] public void TestClearStateOnClearedSwell(bool kiai, TaikoMascotAnimationState expectedStateAfterClear) { - prepareTest(kiai); + prepareDrawableRulesetAndBeatmap(kiai); assertStateAfterResult(new JudgementResult(new Swell(), new TaikoSwellJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Clear); AddUntilStep($"state reverts to {expectedStateAfterClear.ToString().ToLowerInvariant()}", () => allMascotsIn(expectedStateAfterClear)); @@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning scoreProcessor.ApplyBeatmap(Beatmap.Value.Beatmap); } - private void prepareTest(bool kiai) + private void prepareDrawableRulesetAndBeatmap(bool kiai) { AddStep("set beatmap", () => setBeatmap(kiai)); From 3d14b14cfe4b53c78ce0a641e45ad525939fddb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Aug 2022 21:52:55 +0200 Subject: [PATCH 202/532] Use alternative method for checking panel readiness to eliminate bool flag --- osu.Game/Overlays/Mods/ModColumn.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index cf123f0347..b9f7114f74 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -66,8 +66,8 @@ namespace osu.Game.Overlays.Mods private IModHotkeyHandler hotkeyHandler = null!; private Task? latestLoadTask; - private bool itemsLoaded; - internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true && itemsLoaded; + private ICollection? latestLoadedPanels; + internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true && latestLoadedPanels?.All(panel => panel.Parent != null) == true; public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; @@ -133,14 +133,13 @@ namespace osu.Game.Overlays.Mods { cancellationTokenSource?.Cancel(); - var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero)); + var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero)).ToArray(); + latestLoadedPanels = panels; - itemsLoaded = false; latestLoadTask = LoadComponentsAsync(panels, loaded => { ItemsFlow.ChildrenEnumerable = loaded; updateState(); - itemsLoaded = true; }, (cancellationTokenSource = new CancellationTokenSource()).Token); } From d06959e1ddcf0e13b82c1eb1eca97c52ef034e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 17 Aug 2022 22:03:35 +0200 Subject: [PATCH 203/532] Update incorrect xmldoc --- osu.Game/Screens/Ranking/Statistics/StatisticItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index a3c51b4876..e1ea352f1c 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// Creates a new , to be displayed inside a in the results screen. /// - /// The name of the item. Can be to hide the item header. + /// The name of the item. Can be to hide the item header. /// A function returning the content to be displayed. /// Whether this item requires hit events. If true, will not be called if no hit events are available. /// The of this item. This can be thought of as the column dimension of an encompassing . From cb6339a20b50d1c8f250f0ca1588bb03c5341963 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 18 Aug 2022 01:29:03 +0200 Subject: [PATCH 204/532] added slider splitting option --- .../Components/PathControlPointVisualiser.cs | 48 +++++++++++-- .../Sliders/SliderSelectionBlueprint.cs | 70 ++++++++++++++++++- 2 files changed, 111 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 0506e8ab8a..3fb7ec93e6 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private InputManager inputManager; public Action> RemoveControlPointsRequested; + public Action> SplitControlPointsRequested; [Resolved(CanBeNull = true)] private IDistanceSnapProvider snapProvider { get; set; } @@ -104,6 +105,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components return true; } + // The slider can only be split on control points which connect two different slider segments. + private bool splittable(PathControlPointPiece p) => p.ControlPoint.Type.HasValue && p != Pieces[0] && p != Pieces[^1]; + + private bool splitSelected() + { + List toSplit = Pieces.Where(p => p.IsSelected.Value && splittable(p)).Select(p => p.ControlPoint).ToList(); + + // Ensure that there are any points to be split + if (toSplit.Count == 0) + return false; + + changeHandler?.BeginChange(); + SplitControlPointsRequested?.Invoke(toSplit); + changeHandler?.EndChange(); + + // Since pieces are re-used, they will not point to the deleted control points while remaining selected + foreach (var piece in Pieces) + piece.IsSelected.Value = false; + + return true; + } + private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -322,6 +345,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (count == 0) return null; + var splittablePieces = selectedPieces.Where(splittable).ToList(); + int splittableCount = splittablePieces.Count; + List items = new List(); if (!selectedPieces.Contains(Pieces[0])) @@ -333,14 +359,24 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components items.Add(createMenuItemForPathType(PathType.Bezier)); items.Add(createMenuItemForPathType(PathType.Catmull)); - return new MenuItem[] + var menuItems = new MenuItem[splittableCount > 0 ? 3 : 2]; + int i = 0; + + menuItems[i++] = new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, + () => DeleteSelected()); + + if (splittableCount > 0) { - new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => DeleteSelected()), - new OsuMenuItem("Curve type") - { - Items = items - } + menuItems[i++] = new OsuMenuItem($"Split {"control point".ToQuantity(splittableCount, splittableCount > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", + MenuItemType.Destructive, () => splitSelected()); + } + + menuItems[i] = new OsuMenuItem("Curve type") + { + Items = items }; + + return menuItems; } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 794551dab7..70993ef7ac 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; @@ -111,7 +112,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true) { - RemoveControlPointsRequested = removeControlPoints + RemoveControlPointsRequested = removeControlPoints, + SplitControlPointsRequested = splitControlPoints }); base.OnSelected(); @@ -249,6 +251,72 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HitObject.Position += first; } + private void splitControlPoints(List toSplit) + { + // Ensure that there are any points to be split + if (toSplit.Count == 0) + return; + + foreach (var c in toSplit) + { + if (c == controlPoints[0] || c == controlPoints[^1] || c.Type is null) + continue; + + // Split off the section of slider before this control point so the remaining control points to split are in the latter part of the slider. + var splitControlPoints = controlPoints.TakeWhile(current => current != c).ToList(); + + if (splitControlPoints.Count == 0) + continue; + + foreach (var current in splitControlPoints) + { + controlPoints.Remove(current); + } + + splitControlPoints.Add(c); + + // Turn the control points which were split off into a new slider. + var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone(); + samplePoint.Time = HitObject.StartTime; + var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone(); + difficultyPoint.Time = HitObject.StartTime; + + var newSlider = new Slider + { + StartTime = HitObject.StartTime, + Position = HitObject.Position + splitControlPoints[0].Position, + NewCombo = HitObject.NewCombo, + SampleControlPoint = samplePoint, + DifficultyControlPoint = difficultyPoint, + Samples = HitObject.Samples.Select(s => s.With()).ToList(), + RepeatCount = HitObject.RepeatCount, + NodeSamples = HitObject.NodeSamples.Select(n => (IList)n.Select(s => s.With()).ToList()).ToList(), + Path = new SliderPath(splitControlPoints.Select(o => new PathControlPoint(o.Position - splitControlPoints[0].Position, o == splitControlPoints[^1] ? null : o.Type)).ToArray()) + }; + + editorBeatmap.Add(newSlider); + + HitObject.NewCombo = false; + HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance; + HitObject.StartTime += newSlider.SpanDuration; + + // In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider. + if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON) + { + HitObject.Path.ExpectedDistance.Value = null; + } + } + + editorBeatmap.SelectedHitObjects.Clear(); + + // The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position + // So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0) + Vector2 first = controlPoints[0].Position; + foreach (var c in controlPoints) + c.Position -= first; + HitObject.Position += first; + } + private void convertToStream() { if (editorBeatmap == null || beatDivisor == null) From 9735728cf65df91f4276414ee2d749f4235342a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 15:08:09 +0900 Subject: [PATCH 205/532] Reverse conditionals to better define intent in `addSourceClockAdjustments` --- .../Play/MasterGameplayClockContainer.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index ea4f767109..94967df840 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -216,11 +216,11 @@ namespace osu.Game.Screens.Play if (speedAdjustmentsApplied) return; - if (SourceClock is Track track) - { - track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - } + if (SourceClock is not Track track) + return; + + track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); nonGameplayAdjustments.Add(pauseFreqAdjust); nonGameplayAdjustments.Add(UserPlaybackRate); @@ -233,11 +233,11 @@ namespace osu.Game.Screens.Play if (!speedAdjustmentsApplied) return; - if (SourceClock is Track track) - { - track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - } + if (SourceClock is not Track track) + return; + + track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); nonGameplayAdjustments.Remove(pauseFreqAdjust); nonGameplayAdjustments.Remove(UserPlaybackRate); From 7512c126b79b06e84eb23eab38e09a777a14c8f0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 Aug 2022 13:41:22 +0900 Subject: [PATCH 206/532] Upgrade LocalisationAnalyser and disable warning --- .globalconfig | 4 ++++ osu.Game/osu.Game.csproj | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.globalconfig b/.globalconfig index 462dbc74ed..a7b652c454 100644 --- a/.globalconfig +++ b/.globalconfig @@ -53,3 +53,7 @@ dotnet_diagnostic.CA2225.severity = none # Banned APIs dotnet_diagnostic.RS0030.severity = error + +# Temporarily disable analysing CanBeNull = true in NRT contexts due to mobile issues. +# See: https://github.com/ppy/osu/pull/19677 +dotnet_diagnostic.OSUF001.severity = none \ No newline at end of file diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index d67f8415e7..28452c0a74 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -31,7 +31,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 40b1554feab6e5d161c0f1f5315c1048a515a880 Mon Sep 17 00:00:00 2001 From: vun Date: Thu, 18 Aug 2022 14:12:03 +0800 Subject: [PATCH 207/532] Change FindRepetitionInterval to start with one previous encoding --- .../Preprocessing/Colour/Data/CoupledColourEncoding.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs index 1b831eedd8..3f692e9d3d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs @@ -60,14 +60,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// public void FindRepetitionInterval() { - if (Previous?.Previous == null) + if (Previous == null) { RepetitionInterval = max_repetition_interval + 1; return; } - CoupledColourEncoding? other = Previous.Previous; - int interval = 2; + CoupledColourEncoding? other = Previous; + int interval = 1; while (interval < max_repetition_interval) { From e55b94d4121d30db257a9bd231fbd921c59a7c8f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 Aug 2022 15:18:35 +0900 Subject: [PATCH 208/532] Also upgrade tools --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index cbd0231fdb..57694d7f57 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -21,7 +21,7 @@ ] }, "ppy.localisationanalyser.tools": { - "version": "2022.607.0", + "version": "2022.809.0", "commands": [ "localisation" ] From e0edaf996fa36c5ca15414739d34431de878da55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 16:03:37 +0900 Subject: [PATCH 209/532] Test ruleset compatibility during initial startup to avoid runtime errors As we continue to break the ruleset API, it makes more sense to proactively check known changes and bail early during ruleset loading to avoid a user experiencing a crash at a random point during execution. This is a RFC and needs to be tested against known broken rulesets. There might be some other calls we want to add in addition to the ones I've listed. --- osu.Game/Rulesets/RealmRulesetStore.cs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/RealmRulesetStore.cs b/osu.Game/Rulesets/RealmRulesetStore.cs index 62a7a13c07..c014b2d095 100644 --- a/osu.Game/Rulesets/RealmRulesetStore.cs +++ b/osu.Game/Rulesets/RealmRulesetStore.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Game.Beatmaps; using osu.Game.Database; namespace osu.Game.Rulesets @@ -83,17 +84,36 @@ namespace osu.Game.Rulesets r.InstantiationInfo = instanceInfo.InstantiationInfo; r.Available = true; + testRulesetCompatibility(r); + detachedRulesets.Add(r.Clone()); } catch (Exception ex) { r.Available = false; - Logger.Log($"Could not load ruleset {r}: {ex.Message}"); + Logger.Log($"Could not load ruleset {r.Name}. Please check for an update from the developer.", level: LogLevel.Error); + Logger.Log($"Ruleset load failed with {ex.Message}"); } } availableRulesets.AddRange(detachedRulesets.OrderBy(r => r)); }); } + + private void testRulesetCompatibility(RulesetInfo rulesetInfo) + { + // do various operations to ensure that we are in a good state. + // if we can avoid loading the ruleset at this point (rather than erroring later in runtime) then that is preferred. + var instance = rulesetInfo.CreateInstance(); + + instance.CreateAllMods(); + instance.CreateIcon(); + instance.CreateResourceStore(); + + var beatmap = new Beatmap(); + var converter = instance.CreateBeatmapConverter(beatmap); + + instance.CreateBeatmapProcessor(converter.Convert()); + } } } From b0a740071e93ff1a899d969f1fcf8f4a83bf95c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 16:14:38 +0900 Subject: [PATCH 210/532] Centralise logging of failed ruleset loads --- osu.Game/Rulesets/RealmRulesetStore.cs | 4 +--- osu.Game/Rulesets/RulesetStore.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/RealmRulesetStore.cs b/osu.Game/Rulesets/RealmRulesetStore.cs index c014b2d095..7b1f3a3f6c 100644 --- a/osu.Game/Rulesets/RealmRulesetStore.cs +++ b/osu.Game/Rulesets/RealmRulesetStore.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Database; @@ -91,8 +90,7 @@ namespace osu.Game.Rulesets catch (Exception ex) { r.Available = false; - Logger.Log($"Could not load ruleset {r.Name}. Please check for an update from the developer.", level: LogLevel.Error); - Logger.Log($"Ruleset load failed with {ex.Message}"); + LogFailedLoad(r.Name, ex); } } diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 6b3e43cc1c..fdbcd0ed1e 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -138,7 +138,7 @@ namespace osu.Game.Rulesets } catch (Exception e) { - Logger.Error(e, $"Failed to load ruleset {filename}"); + LogFailedLoad(filename, e); } } @@ -158,7 +158,7 @@ namespace osu.Game.Rulesets } catch (Exception e) { - Logger.Error(e, $"Failed to add ruleset {assembly}"); + LogFailedLoad(assembly.FullName, e); } } @@ -173,6 +173,12 @@ namespace osu.Game.Rulesets AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly; } + protected void LogFailedLoad(string name, Exception exception) + { + Logger.Log($"Could not load ruleset {name}. Please check for an update from the developer.", level: LogLevel.Error); + Logger.Log($"Ruleset load failed: {exception}"); + } + #region Implementation of IRulesetStore IRulesetInfo? IRulesetStore.GetRuleset(int id) => GetRuleset(id); From bb46f72f9ee3e48b6d723e634119eb737cb086fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 16:17:50 +0900 Subject: [PATCH 211/532] Fix `Pippidon` crash on empty beatmap conversion --- .../Beatmaps/PippidonBeatmapConverter.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs index 8f0b31ef1b..0a4fa84ce1 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs @@ -21,8 +21,11 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps public PippidonBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) : base(beatmap, ruleset) { - minPosition = beatmap.HitObjects.Min(getUsablePosition); - maxPosition = beatmap.HitObjects.Max(getUsablePosition); + if (beatmap.HitObjects.Any()) + { + minPosition = beatmap.HitObjects.Min(getUsablePosition); + maxPosition = beatmap.HitObjects.Max(getUsablePosition); + } } public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition && h is IHasYPosition); From 48fac9f8a55bf1e63260d905b2fe48ea0fc148d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 16:50:08 +0900 Subject: [PATCH 212/532] Fix taiko drum rolls with zero length being placeable in editor Addresses https://github.com/ppy/osu/discussions/19808. --- .../Edit/Blueprints/TaikoSpanPlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index e49043e58e..23a005190a 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints return; base.OnMouseUp(e); - EndPlacement(true); + EndPlacement(spanPlacementObject.Duration > 0); } public override void UpdateTimeAndPosition(SnapResult result) From ad28bfc9b2495752857f66d1ba346a2a55da3338 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 17:05:09 +0900 Subject: [PATCH 213/532] Fix taiko blueprints displaying incorrectly for drum rolls --- .../Objects/Drawables/DrawableDrumRoll.cs | 3 +++ .../Skinning/Legacy/LegacyCirclePiece.cs | 4 ++++ .../Skinning/Legacy/LegacyDrumRoll.cs | 18 +++++++++++++++--- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 04ed6d0b87..68e334332e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osuTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Events; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -30,6 +31,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// private const int rolling_hits_for_engaged_colour = 5; + public override Quad ScreenSpaceDrawQuad => MainPiece.Drawable.ScreenSpaceDrawQuad; + /// /// Rolling number of tick hits. This increases for hits and decreases for misses. /// diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs index 7f813e7b27..399bd9260d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; @@ -20,6 +21,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { private Drawable backgroundLayer; + // required for editor blueprints (not sure why these circle pieces are zero size). + public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad; + public LegacyCirclePiece() { RelativeSizeAxes = Axes.Both; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs index d3bf70e603..040d8ff965 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Graphics; @@ -16,11 +17,22 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public class LegacyDrumRoll : CompositeDrawable, IHasAccentColour { + public override Quad ScreenSpaceDrawQuad + { + get + { + var headDrawQuad = headCircle.ScreenSpaceDrawQuad; + var tailDrawQuad = tailCircle.ScreenSpaceDrawQuad; + + return new Quad(headDrawQuad.TopLeft, tailDrawQuad.TopRight, headDrawQuad.BottomLeft, tailDrawQuad.BottomRight); + } + } + private LegacyCirclePiece headCircle; private Sprite body; - private Sprite end; + private Sprite tailCircle; public LegacyDrumRoll() { @@ -32,7 +44,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { InternalChildren = new Drawable[] { - end = new Sprite + tailCircle = new Sprite { Anchor = Anchor.CentreRight, Origin = Anchor.CentreLeft, @@ -82,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy headCircle.AccentColour = colour; body.Colour = colour; - end.Colour = colour; + tailCircle.Colour = colour; } } } From 5d8d584afb802369c03e602cae5fc4f704073d3f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 18 Aug 2022 18:08:46 +0900 Subject: [PATCH 214/532] Fix some backwards asserts --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index b7f91c22f4..c01b2576e8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -199,14 +199,14 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate); addSeekStep(1000); - AddAssert("progress almost same", () => expectedProgress, () => Is.EqualTo(drawableSpinner.Progress).Within(0.05)); - AddAssert("spm almost same", () => expectedSpm, () => Is.EqualTo(drawableSpinner.SpinsPerMinute.Value).Within(2.0)); + AddAssert("progress almost same", () => drawableSpinner.Progress, () => Is.EqualTo(expectedProgress).Within(0.05)); + AddAssert("spm almost same", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(expectedSpm).Within(2.0)); } private void addSeekStep(double time) { AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time)); - AddUntilStep("wait for seek to finish", () => time, () => Is.EqualTo(Player.DrawableRuleset.FrameStableClock.CurrentTime).Within(100)); + AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(100)); } private void transformReplay(Func replayTransformation) => AddStep("set replay", () => From 32e127a6fa995d63363d95d97e7089a5039ad895 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 15:39:28 +0900 Subject: [PATCH 215/532] Add `FramedBeatmapClock` Expose `IsCoupled` in `FramedBeatmapClock` for now to provide editor compatibility --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 167 ++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 osu.Game/Beatmaps/FramedBeatmapClock.cs diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs new file mode 100644 index 0000000000..925cc88fbd --- /dev/null +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -0,0 +1,167 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Timing; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Screens.Play; + +namespace osu.Game.Beatmaps +{ + public class FramedBeatmapClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock + { + /// + /// The length of the underlying beatmap track. Will default to 60 seconds if unavailable. + /// + public double TrackLength => Track?.Length ?? 60000; + + /// + /// The underlying beatmap track, if available. + /// + public Track? Track { get; private set; } // TODO: virtual rather than null? + + private readonly OffsetCorrectionClock userGlobalOffsetClock; + private readonly OffsetCorrectionClock platformOffsetClock; + private readonly OffsetCorrectionClock finalOffsetClock; + + private Bindable userAudioOffset = null!; + + private IDisposable? beatmapOffsetSubscription; + + private readonly DecoupleableInterpolatingFramedClock decoupledClock; + + private double totalAppliedOffset => userGlobalOffsetClock.RateAdjustedOffset + finalOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; + + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + public bool IsCoupled + { + get => decoupledClock.IsCoupled; + set => decoupledClock.IsCoupled = value; + } + + public FramedBeatmapClock(IClock? sourceClock = null) + { + // TODO: Unused for now? + var pauseFreqAdjust = new BindableDouble(1); + + // A decoupled clock is used to ensure precise time values even when the host audio subsystem is not reporting + // high precision times (on windows there's generally only 5-10ms reporting intervals, as an example). + decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; + decoupledClock.ChangeSource(sourceClock); + + // Audio timings in general with newer BASS versions don't match stable. + // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. + platformOffsetClock = new OffsetCorrectionClock(decoupledClock, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; + + // User global offset (set in settings) should also be applied. + userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, pauseFreqAdjust); + + // User per-beatmap offset will be applied to this final clock. + finalOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock, pauseFreqAdjust); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); + userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); + + beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( + r => r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings, + settings => settings.Offset, + val => finalOffsetClock.Offset = val); + } + + protected override void Update() + { + base.Update(); + finalOffsetClock.ProcessFrame(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + beatmapOffsetSubscription?.Dispose(); + } + + #region Delegation of IAdjustableClock / ISourceChangeableClock to decoupled clock. + + public void ChangeSource(IClock? source) + { + Track = source as Track; + decoupledClock.ChangeSource(source); + } + + public IClock? Source => decoupledClock.Source; + + public void Reset() + { + decoupledClock.Reset(); + finalOffsetClock.ProcessFrame(); + } + + public void Start() + { + decoupledClock.Start(); + finalOffsetClock.ProcessFrame(); + } + + public void Stop() + { + decoupledClock.Stop(); + finalOffsetClock.ProcessFrame(); + } + + public bool Seek(double position) + { + bool success = decoupledClock.Seek(position - totalAppliedOffset); + finalOffsetClock.ProcessFrame(); + + return success; + } + + public void ResetSpeedAdjustments() => decoupledClock.ResetSpeedAdjustments(); + + public double Rate + { + get => decoupledClock.Rate; + set => decoupledClock.Rate = value; + } + + #endregion + + #region Delegation of IFrameBasedClock to clock with all offsets applied + + public double CurrentTime => finalOffsetClock.CurrentTime; + + public bool IsRunning => finalOffsetClock.IsRunning; + + public void ProcessFrame() + { + // Noop to ensure an external consumer doesn't process the internal clock an extra time. + } + + public double ElapsedFrameTime => finalOffsetClock.ElapsedFrameTime; + + public double FramesPerSecond => finalOffsetClock.FramesPerSecond; + + public FrameTimeInfo TimeInfo => finalOffsetClock.TimeInfo; + + #endregion + } +} From 6003afafc790f20d78f884cde36b8cfa78980fe6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 14:52:47 +0900 Subject: [PATCH 216/532] Use `FramedBeatmapClock` in `GameplayClockContainer` --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 98 +++++++++++++------ .../Screens/Play/GameplayClockContainer.cs | 86 +++++++--------- .../Play/MasterGameplayClockContainer.cs | 78 +++------------ 3 files changed, 113 insertions(+), 149 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 925cc88fbd..f3219b972e 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Track; @@ -16,6 +17,8 @@ namespace osu.Game.Beatmaps { public class FramedBeatmapClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock { + private readonly bool applyOffsets; + /// /// The length of the underlying beatmap track. Will default to 60 seconds if unavailable. /// @@ -26,9 +29,16 @@ namespace osu.Game.Beatmaps /// public Track? Track { get; private set; } // TODO: virtual rather than null? - private readonly OffsetCorrectionClock userGlobalOffsetClock; - private readonly OffsetCorrectionClock platformOffsetClock; - private readonly OffsetCorrectionClock finalOffsetClock; + /// + /// The total frequency adjustment from pause transforms. Should eventually be handled in a better way. + /// + public readonly BindableDouble ExternalPauseFrequencyAdjust = new BindableDouble(1); + + private readonly OffsetCorrectionClock? userGlobalOffsetClock; + private readonly OffsetCorrectionClock? platformOffsetClock; + private readonly OffsetCorrectionClock? userBeatmapOffsetClock; + + private readonly IFrameBasedClock finalClockSource; private Bindable userAudioOffset = null!; @@ -36,7 +46,20 @@ namespace osu.Game.Beatmaps private readonly DecoupleableInterpolatingFramedClock decoupledClock; - private double totalAppliedOffset => userGlobalOffsetClock.RateAdjustedOffset + finalOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; + private double totalAppliedOffset + { + get + { + if (!applyOffsets) + return 0; + + Debug.Assert(userGlobalOffsetClock != null); + Debug.Assert(userBeatmapOffsetClock != null); + Debug.Assert(platformOffsetClock != null); + + return userGlobalOffsetClock.RateAdjustedOffset + userBeatmapOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; + } + } [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -53,44 +76,59 @@ namespace osu.Game.Beatmaps set => decoupledClock.IsCoupled = value; } - public FramedBeatmapClock(IClock? sourceClock = null) + public FramedBeatmapClock(IClock? sourceClock = null, bool applyOffsets = false) { - // TODO: Unused for now? - var pauseFreqAdjust = new BindableDouble(1); + this.applyOffsets = applyOffsets; // A decoupled clock is used to ensure precise time values even when the host audio subsystem is not reporting // high precision times (on windows there's generally only 5-10ms reporting intervals, as an example). decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; decoupledClock.ChangeSource(sourceClock); - // Audio timings in general with newer BASS versions don't match stable. - // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new OffsetCorrectionClock(decoupledClock, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; + if (applyOffsets) + { + // Audio timings in general with newer BASS versions don't match stable. + // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. + platformOffsetClock = new OffsetCorrectionClock(decoupledClock, ExternalPauseFrequencyAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; - // User global offset (set in settings) should also be applied. - userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, pauseFreqAdjust); + // User global offset (set in settings) should also be applied. + userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, ExternalPauseFrequencyAdjust); - // User per-beatmap offset will be applied to this final clock. - finalOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock, pauseFreqAdjust); + // User per-beatmap offset will be applied to this final clock. + finalClockSource = userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock, ExternalPauseFrequencyAdjust); + } + else + { + finalClockSource = decoupledClock; + } } protected override void LoadComplete() { base.LoadComplete(); - userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); - userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); + if (applyOffsets) + { + Debug.Assert(userBeatmapOffsetClock != null); + Debug.Assert(userGlobalOffsetClock != null); - beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( - r => r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings, - settings => settings.Offset, - val => finalOffsetClock.Offset = val); + userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); + userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); + + beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( + r => r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings, + settings => settings.Offset, + val => + { + userBeatmapOffsetClock.Offset = val; + }); + } } protected override void Update() { base.Update(); - finalOffsetClock.ProcessFrame(); + finalClockSource.ProcessFrame(); } protected override void Dispose(bool isDisposing) @@ -112,25 +150,25 @@ namespace osu.Game.Beatmaps public void Reset() { decoupledClock.Reset(); - finalOffsetClock.ProcessFrame(); + finalClockSource.ProcessFrame(); } public void Start() { decoupledClock.Start(); - finalOffsetClock.ProcessFrame(); + finalClockSource.ProcessFrame(); } public void Stop() { decoupledClock.Stop(); - finalOffsetClock.ProcessFrame(); + finalClockSource.ProcessFrame(); } public bool Seek(double position) { bool success = decoupledClock.Seek(position - totalAppliedOffset); - finalOffsetClock.ProcessFrame(); + finalClockSource.ProcessFrame(); return success; } @@ -147,20 +185,20 @@ namespace osu.Game.Beatmaps #region Delegation of IFrameBasedClock to clock with all offsets applied - public double CurrentTime => finalOffsetClock.CurrentTime; + public double CurrentTime => finalClockSource.CurrentTime; - public bool IsRunning => finalOffsetClock.IsRunning; + public bool IsRunning => finalClockSource.IsRunning; public void ProcessFrame() { // Noop to ensure an external consumer doesn't process the internal clock an extra time. } - public double ElapsedFrameTime => finalOffsetClock.ElapsedFrameTime; + public double ElapsedFrameTime => finalClockSource.ElapsedFrameTime; - public double FramesPerSecond => finalOffsetClock.FramesPerSecond; + public double FramesPerSecond => finalClockSource.FramesPerSecond; - public FrameTimeInfo TimeInfo => finalOffsetClock.TimeInfo; + public FrameTimeInfo TimeInfo => finalClockSource.TimeInfo; #endregion } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index ac846b45c4..de28f86824 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -11,12 +11,14 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Timing; using osu.Framework.Utils; +using osu.Game.Beatmaps; namespace osu.Game.Screens.Play { /// /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use. /// + [Cached(typeof(IGameplayClock))] public class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock { /// @@ -45,44 +47,34 @@ namespace osu.Game.Screens.Play public virtual IEnumerable NonGameplayAdjustments => Enumerable.Empty(); - /// - /// The final clock which is exposed to gameplay components. - /// - protected IFrameBasedClock FramedClock { get; private set; } - private readonly BindableBool isPaused = new BindableBool(true); /// /// The adjustable source clock used for gameplay. Should be used for seeks and clock control. + /// This is the final clock exposed to gameplay components as an . /// - private readonly DecoupleableInterpolatingFramedClock decoupledClock; + protected readonly FramedBeatmapClock GameplayClock; + + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; /// /// Creates a new . /// /// The source used for timing. - public GameplayClockContainer(IClock sourceClock) + /// Whether to apply platform, user and beatmap offsets to the mix. + public GameplayClockContainer(IClock sourceClock, bool applyOffsets = false) { SourceClock = sourceClock; RelativeSizeAxes = Axes.Both; - decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + InternalChildren = new Drawable[] + { + GameplayClock = new FramedBeatmapClock(sourceClock, applyOffsets) { IsCoupled = false }, + Content + }; + IsPaused.BindValueChanged(OnIsPausedChanged); - - // this will be replaced during load, but non-null for tests which don't add this component to the hierarchy. - FramedClock = new FramedClock(); - } - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - - FramedClock = CreateGameplayClock(decoupledClock); - - dependencies.CacheAs(this); - - return dependencies; } /// @@ -92,13 +84,13 @@ namespace osu.Game.Screens.Play { ensureSourceClockSet(); - if (!decoupledClock.IsRunning) + if (!GameplayClock.IsRunning) { // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time // This accounts for the clock source potentially taking time to enter a completely stopped state - Seek(FramedClock.CurrentTime); + Seek(GameplayClock.CurrentTime); - decoupledClock.Start(); + GameplayClock.Start(); } isPaused.Value = false; @@ -112,10 +104,10 @@ namespace osu.Game.Screens.Play { Logger.Log($"{nameof(GameplayClockContainer)} seeking to {time}"); - decoupledClock.Seek(time); + GameplayClock.Seek(time); // Manually process to make sure the gameplay clock is correctly updated after a seek. - FramedClock.ProcessFrame(); + GameplayClock.ProcessFrame(); OnSeek?.Invoke(); } @@ -132,7 +124,7 @@ namespace osu.Game.Screens.Play public void Reset(bool startClock = false) { // Manually stop the source in order to not affect the IsPaused state. - decoupledClock.Stop(); + GameplayClock.Stop(); if (!IsPaused.Value || startClock) Start(); @@ -145,10 +137,10 @@ namespace osu.Game.Screens.Play /// Changes the source clock. /// /// The new source. - protected void ChangeSource(IClock sourceClock) => decoupledClock.ChangeSource(SourceClock = sourceClock); + protected void ChangeSource(IClock sourceClock) => GameplayClock.ChangeSource(SourceClock = sourceClock); /// - /// Ensures that the is set to , if it hasn't been given a source yet. + /// Ensures that the is set to , if it hasn't been given a source yet. /// This is usually done before a seek to avoid accidentally seeking only the adjustable source in decoupled mode, /// but not the actual source clock. /// That will pretty much only happen on the very first call of this method, as the source clock is passed in the constructor, @@ -156,40 +148,30 @@ namespace osu.Game.Screens.Play /// private void ensureSourceClockSet() { - if (decoupledClock.Source == null) + if (GameplayClock.Source == null) ChangeSource(SourceClock); } protected override void Update() { if (!IsPaused.Value) - FramedClock.ProcessFrame(); + GameplayClock.ProcessFrame(); base.Update(); } /// - /// Invoked when the value of is changed to start or stop the clock. + /// Invoked when the value of is changed to start or stop the clock. /// /// Whether the clock should now be paused. protected virtual void OnIsPausedChanged(ValueChangedEvent isPaused) { if (isPaused.NewValue) - decoupledClock.Stop(); + GameplayClock.Stop(); else - decoupledClock.Start(); + GameplayClock.Start(); } - /// - /// Creates the final which is exposed via DI to be used by gameplay components. - /// - /// - /// Any intermediate clocks such as platform offsets should be applied here. - /// - /// The providing the source time. - /// The final . - protected virtual IFrameBasedClock CreateGameplayClock(IFrameBasedClock source) => source; - #region IAdjustableClock bool IAdjustableClock.Seek(double position) @@ -204,15 +186,15 @@ namespace osu.Game.Screens.Play double IAdjustableClock.Rate { - get => FramedClock.Rate; + get => GameplayClock.Rate; set => throw new NotSupportedException(); } - public double Rate => FramedClock.Rate; + public double Rate => GameplayClock.Rate; - public double CurrentTime => FramedClock.CurrentTime; + public double CurrentTime => GameplayClock.CurrentTime; - public bool IsRunning => FramedClock.IsRunning; + public bool IsRunning => GameplayClock.IsRunning; #endregion @@ -221,11 +203,11 @@ namespace osu.Game.Screens.Play // Handled via update. Don't process here to safeguard from external usages potentially processing frames additional times. } - public double ElapsedFrameTime => FramedClock.ElapsedFrameTime; + public double ElapsedFrameTime => GameplayClock.ElapsedFrameTime; - public double FramesPerSecond => FramedClock.FramesPerSecond; + public double FramesPerSecond => GameplayClock.FramesPerSecond; - public FrameTimeInfo TimeInfo => FramedClock.TimeInfo; + public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo; public double TrueGameplayRate { diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index d26f0c6311..51f57f27b8 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework; -using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; @@ -13,8 +11,6 @@ using osu.Framework.Graphics; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Configuration; -using osu.Game.Database; namespace osu.Game.Screens.Play { @@ -43,28 +39,10 @@ namespace osu.Game.Screens.Play Precision = 0.1, }; - private double totalAppliedOffset => userBeatmapOffsetClock.RateAdjustedOffset + userGlobalOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; - - private readonly BindableDouble pauseFreqAdjust = new BindableDouble(); // Important that this starts at zero, matching the paused state of the clock. - private readonly WorkingBeatmap beatmap; - private OffsetCorrectionClock userGlobalOffsetClock = null!; - private OffsetCorrectionClock userBeatmapOffsetClock = null!; - private OffsetCorrectionClock platformOffsetClock = null!; - - private Bindable userAudioOffset = null!; - - private IDisposable? beatmapOffsetSubscription; - private readonly double skipTargetTime; - [Resolved] - private RealmAccess realm { get; set; } = null!; - - [Resolved] - private OsuConfigManager config { get; set; } = null!; - private readonly List> nonGameplayAdjustments = new List>(); public override IEnumerable NonGameplayAdjustments => nonGameplayAdjustments.Select(b => b.Value); @@ -75,7 +53,7 @@ namespace osu.Game.Screens.Play /// The beatmap to be used for time and metadata references. /// The latest time which should be used when introducing gameplay. Will be used when skipping forward. public MasterGameplayClockContainer(WorkingBeatmap beatmap, double skipTargetTime) - : base(beatmap.Track) + : base(beatmap.Track, true) { this.beatmap = beatmap; this.skipTargetTime = skipTargetTime; @@ -85,14 +63,6 @@ namespace osu.Game.Screens.Play { base.LoadComplete(); - userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); - userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); - - beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( - r => r.Find(beatmap.BeatmapInfo.ID)?.UserSettings, - settings => settings.Offset, - val => userBeatmapOffsetClock.Offset = val); - // Reset may have been called externally before LoadComplete. // If it was, and the clock is in a playing state, we want to ensure that it isn't stopped here. bool isStarted = !IsPaused.Value; @@ -133,14 +103,14 @@ namespace osu.Game.Screens.Play // During normal operation, the source is stopped after performing a frequency ramp. if (isPaused.NewValue) { - this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => + this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 200, Easing.Out).OnComplete(_ => { if (IsPaused.Value == isPaused.NewValue) base.OnIsPausedChanged(isPaused); }); } else - this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); + this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 200, Easing.In); } else { @@ -148,11 +118,11 @@ namespace osu.Game.Screens.Play base.OnIsPausedChanged(isPaused); // If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations. - pauseFreqAdjust.Value = isPaused.NewValue ? 0 : 1; + GameplayClock.ExternalPauseFrequencyAdjust.Value = isPaused.NewValue ? 0 : 1; // We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment. // Without doing this, an initial seek may be performed with the wrong offset. - FramedClock.ProcessFrame(); + GameplayClock.ProcessFrame(); } } @@ -162,48 +132,23 @@ namespace osu.Game.Screens.Play base.Start(); } - /// - /// Seek to a specific time in gameplay. - /// - /// - /// Adjusts for any offsets which have been applied (so the seek may not be the expected point in time on the underlying audio track). - /// - /// The destination time to seek to. - public override void Seek(double time) - { - // remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track. - // we may want to consider reversing the application of offsets in the future as it may feel more correct. - base.Seek(time - totalAppliedOffset); - } - /// /// Skip forward to the next valid skip point. /// public void Skip() { - if (FramedClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME) + if (GameplayClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME) return; double skipTarget = skipTargetTime - MINIMUM_SKIP_TIME; - if (FramedClock.CurrentTime < 0 && skipTarget > 6000) + if (GameplayClock.CurrentTime < 0 && skipTarget > 6000) // double skip exception for storyboards with very long intros skipTarget = 0; Seek(skipTarget); } - protected override IFrameBasedClock CreateGameplayClock(IFrameBasedClock source) - { - // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. - // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new OffsetCorrectionClock(source, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; - - // the final usable gameplay clock with user-set offsets applied. - userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, pauseFreqAdjust); - return userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock, pauseFreqAdjust); - } - /// /// Changes the backing clock to avoid using the originally provided track. /// @@ -224,10 +169,10 @@ namespace osu.Game.Screens.Play if (SourceClock is not Track track) return; - track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + track.AddAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust); track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - nonGameplayAdjustments.Add(pauseFreqAdjust); + nonGameplayAdjustments.Add(GameplayClock.ExternalPauseFrequencyAdjust); nonGameplayAdjustments.Add(UserPlaybackRate); speedAdjustmentsApplied = true; @@ -241,10 +186,10 @@ namespace osu.Game.Screens.Play if (SourceClock is not Track track) return; - track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + track.RemoveAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust); track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - nonGameplayAdjustments.Remove(pauseFreqAdjust); + nonGameplayAdjustments.Remove(GameplayClock.ExternalPauseFrequencyAdjust); nonGameplayAdjustments.Remove(UserPlaybackRate); speedAdjustmentsApplied = false; @@ -253,7 +198,6 @@ namespace osu.Game.Screens.Play protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - beatmapOffsetSubscription?.Dispose(); removeSourceClockAdjustments(); } From bcc153f7387c0eb8273990397d6e9bb113f698b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 15:01:47 +0900 Subject: [PATCH 217/532] Add xmldoc and reorganise `FramedBeatmapClock` --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 45 +++++++++++++++---------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index f3219b972e..90ff0588a6 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -15,6 +15,15 @@ using osu.Game.Screens.Play; namespace osu.Game.Beatmaps { + /// + /// A clock intended to be the single source-of-truth for beatmap timing. + /// + /// It provides some functionality: + /// - Applies (and tracks changes of) beatmap, user, and platform offsets. + /// - Adjusts operations to account for said offsets, seeking in raw time values. + /// - Exposes track length. + /// - Allows changing the source to a new track (for cases like editor track updating). + /// public class FramedBeatmapClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock { private readonly bool applyOffsets; @@ -46,21 +55,6 @@ namespace osu.Game.Beatmaps private readonly DecoupleableInterpolatingFramedClock decoupledClock; - private double totalAppliedOffset - { - get - { - if (!applyOffsets) - return 0; - - Debug.Assert(userGlobalOffsetClock != null); - Debug.Assert(userBeatmapOffsetClock != null); - Debug.Assert(platformOffsetClock != null); - - return userGlobalOffsetClock.RateAdjustedOffset + userBeatmapOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; - } - } - [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -131,10 +125,19 @@ namespace osu.Game.Beatmaps finalClockSource.ProcessFrame(); } - protected override void Dispose(bool isDisposing) + private double totalAppliedOffset { - base.Dispose(isDisposing); - beatmapOffsetSubscription?.Dispose(); + get + { + if (!applyOffsets) + return 0; + + Debug.Assert(userGlobalOffsetClock != null); + Debug.Assert(userBeatmapOffsetClock != null); + Debug.Assert(platformOffsetClock != null); + + return userGlobalOffsetClock.RateAdjustedOffset + userBeatmapOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; + } } #region Delegation of IAdjustableClock / ISourceChangeableClock to decoupled clock. @@ -201,5 +204,11 @@ namespace osu.Game.Beatmaps public FrameTimeInfo TimeInfo => finalClockSource.TimeInfo; #endregion + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + beatmapOffsetSubscription?.Dispose(); + } } } From 728cd96508ebdc0b19af16f3ac620e45d44704b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 16:32:43 +0900 Subject: [PATCH 218/532] Update `TestSceneLeadIn` to use new assert style --- .../Visual/Gameplay/TestSceneLeadIn.cs | 31 +++++-------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index 0d80d29cab..25251bf1d6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -19,7 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneLeadIn : RateAdjustedBeatmapTestScene { - private LeadInPlayer player; + private LeadInPlayer player = null!; private const double lenience_ms = 10; @@ -36,11 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay BeatmapInfo = { AudioLeadIn = leadIn } }); - AddStep("check first frame time", () => - { - Assert.That(player.FirstFrameClockTime, Is.Not.Null); - Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms)); - }); + checkFirstFrameTime(expectedStartTime); } [TestCase(1000, 0)] @@ -59,11 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard); - AddStep("check first frame time", () => - { - Assert.That(player.FirstFrameClockTime, Is.Not.Null); - Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms)); - }); + checkFirstFrameTime(expectedStartTime); } [TestCase(1000, 0, false)] @@ -97,14 +87,13 @@ namespace osu.Game.Tests.Visual.Gameplay loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard); - AddStep("check first frame time", () => - { - Assert.That(player.FirstFrameClockTime, Is.Not.Null); - Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms)); - }); + checkFirstFrameTime(expectedStartTime); } - private void loadPlayerWithBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + private void checkFirstFrameTime(double expectedStartTime) => + AddAssert("check first frame time", () => player.FirstFrameClockTime, () => Is.EqualTo(expectedStartTime).Within(lenience_ms)); + + private void loadPlayerWithBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) { AddStep("create player", () => { @@ -126,12 +115,8 @@ namespace osu.Game.Tests.Visual.Gameplay public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; - public double GameplayStartTime => DrawableRuleset.GameplayStartTime; - public double FirstHitObjectTime => DrawableRuleset.Objects.First().StartTime; - public double GameplayClockTime => GameplayClockContainer.CurrentTime; - protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); From 2c6fd1ec6e681c2599a81775784e80f4ae866f26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 18:10:20 +0900 Subject: [PATCH 219/532] Fix `GameplayClockContainer potentially resetting external seeks --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- osu.Game/Screens/Play/GameplayClockContainer.cs | 8 ++++---- osu.Game/Screens/Play/HUD/SongProgress.cs | 2 +- osu.Game/Screens/Play/IGameplayClock.cs | 6 +++--- .../Screens/Play/MasterGameplayClockContainer.cs | 14 +------------- 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index e41802808f..ab9558e77d 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -281,7 +281,7 @@ namespace osu.Game.Rulesets.UI } } - public double? StartTime => parentGameplayClock?.StartTime; + public double StartTime => parentGameplayClock?.StartTime ?? 0; public IEnumerable NonGameplayAdjustments => parentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty(); diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index de28f86824..33f4f26dfb 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -40,10 +40,10 @@ namespace osu.Game.Screens.Play /// The time from which the clock should start. Will be seeked to on calling . /// /// - /// If not set, a value of zero will be used. - /// Importantly, the value will be inferred from the current ruleset in unless specified. + /// By default, a value of zero will be used. + /// Importantly, the value will be inferred from the current beatmap in by default. /// - public double? StartTime { get; set; } + public double StartTime { get; set; } = 0; public virtual IEnumerable NonGameplayAdjustments => Enumerable.Empty(); @@ -130,7 +130,7 @@ namespace osu.Game.Screens.Play Start(); ensureSourceClockSet(); - Seek(StartTime ?? 0); + Seek(StartTime); } /// diff --git a/osu.Game/Screens/Play/HUD/SongProgress.cs b/osu.Game/Screens/Play/HUD/SongProgress.cs index f368edbfb9..0b6494bd8a 100644 --- a/osu.Game/Screens/Play/HUD/SongProgress.cs +++ b/osu.Game/Screens/Play/HUD/SongProgress.cs @@ -82,7 +82,7 @@ namespace osu.Game.Screens.Play.HUD if (isInIntro) { - double introStartTime = GameplayClock.StartTime ?? 0; + double introStartTime = GameplayClock.StartTime; double introOffsetCurrent = currentTime - introStartTime; double introDuration = FirstHitTime - introStartTime; diff --git a/osu.Game/Screens/Play/IGameplayClock.cs b/osu.Game/Screens/Play/IGameplayClock.cs index 5f54ce691a..ea567090ad 100644 --- a/osu.Game/Screens/Play/IGameplayClock.cs +++ b/osu.Game/Screens/Play/IGameplayClock.cs @@ -19,10 +19,10 @@ namespace osu.Game.Screens.Play /// The time from which the clock should start. Will be seeked to on calling . /// /// - /// If not set, a value of zero will be used. - /// Importantly, the value will be inferred from the current ruleset in unless specified. + /// By default, a value of zero will be used. + /// Importantly, the value will be inferred from the current beatmap in by default. /// - double? StartTime { get; } + double StartTime { get; } /// /// All adjustments applied to this clock which don't come from gameplay or mods. diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 51f57f27b8..eca0c92f8f 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -57,20 +57,8 @@ namespace osu.Game.Screens.Play { this.beatmap = beatmap; this.skipTargetTime = skipTargetTime; - } - protected override void LoadComplete() - { - base.LoadComplete(); - - // Reset may have been called externally before LoadComplete. - // If it was, and the clock is in a playing state, we want to ensure that it isn't stopped here. - bool isStarted = !IsPaused.Value; - - // If a custom start time was not specified, calculate the best value to use. - StartTime ??= findEarliestStartTime(); - - Reset(startClock: isStarted); + StartTime = findEarliestStartTime(); } private double findEarliestStartTime() From 343efa1d119dfdca5a342f70be5a563b2b8062ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Aug 2022 18:30:25 +0900 Subject: [PATCH 220/532] Split `OffsetCorrectionClock` out of `MasterGameplayClockContainer` --- .../Play/MasterGameplayClockContainer.cs | 67 ++++--------------- .../Screens/Play/OffsetCorrectionClock.cs | 53 +++++++++++++++ 2 files changed, 65 insertions(+), 55 deletions(-) create mode 100644 osu.Game/Screens/Play/OffsetCorrectionClock.cs diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 94967df840..d26f0c6311 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -49,9 +49,10 @@ namespace osu.Game.Screens.Play private readonly WorkingBeatmap beatmap; - private HardwareCorrectionOffsetClock userGlobalOffsetClock = null!; - private HardwareCorrectionOffsetClock userBeatmapOffsetClock = null!; - private HardwareCorrectionOffsetClock platformOffsetClock = null!; + private OffsetCorrectionClock userGlobalOffsetClock = null!; + private OffsetCorrectionClock userBeatmapOffsetClock = null!; + private OffsetCorrectionClock platformOffsetClock = null!; + private Bindable userAudioOffset = null!; private IDisposable? beatmapOffsetSubscription; @@ -64,6 +65,10 @@ namespace osu.Game.Screens.Play [Resolved] private OsuConfigManager config { get; set; } = null!; + private readonly List> nonGameplayAdjustments = new List>(); + + public override IEnumerable NonGameplayAdjustments => nonGameplayAdjustments.Select(b => b.Value); + /// /// Create a new master gameplay clock container. /// @@ -192,11 +197,11 @@ namespace osu.Game.Screens.Play { // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new HardwareCorrectionOffsetClock(source, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; + platformOffsetClock = new OffsetCorrectionClock(source, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; // the final usable gameplay clock with user-set offsets applied. - userGlobalOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock, pauseFreqAdjust); - return userBeatmapOffsetClock = new HardwareCorrectionOffsetClock(userGlobalOffsetClock, pauseFreqAdjust); + userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, pauseFreqAdjust); + return userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock, pauseFreqAdjust); } /// @@ -254,55 +259,7 @@ namespace osu.Game.Screens.Play ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo; IClock IBeatSyncProvider.Clock => this; + ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => beatmap.TrackLoaded ? beatmap.Track.CurrentAmplitudes : ChannelAmplitudes.Empty; - - private class HardwareCorrectionOffsetClock : FramedOffsetClock - { - private readonly BindableDouble pauseRateAdjust; - - private double offset; - - public new double Offset - { - get => offset; - set - { - if (value == offset) - return; - - offset = value; - - updateOffset(); - } - } - - public double RateAdjustedOffset => base.Offset; - - public HardwareCorrectionOffsetClock(IClock source, BindableDouble pauseRateAdjust) - : base(source) - { - this.pauseRateAdjust = pauseRateAdjust; - } - - public override void ProcessFrame() - { - base.ProcessFrame(); - updateOffset(); - } - - private void updateOffset() - { - // changing this during the pause transform effect will cause a potentially large offset to be suddenly applied as we approach zero rate. - if (pauseRateAdjust.Value == 1) - { - // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. - base.Offset = Offset * Rate; - } - } - } - - private readonly List> nonGameplayAdjustments = new List>(); - - public override IEnumerable NonGameplayAdjustments => nonGameplayAdjustments.Select(b => b.Value); } } diff --git a/osu.Game/Screens/Play/OffsetCorrectionClock.cs b/osu.Game/Screens/Play/OffsetCorrectionClock.cs new file mode 100644 index 0000000000..207980f45c --- /dev/null +++ b/osu.Game/Screens/Play/OffsetCorrectionClock.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Timing; + +namespace osu.Game.Screens.Play +{ + public class OffsetCorrectionClock : FramedOffsetClock + { + private readonly BindableDouble pauseRateAdjust; + + private double offset; + + public new double Offset + { + get => offset; + set + { + if (value == offset) + return; + + offset = value; + + updateOffset(); + } + } + + public double RateAdjustedOffset => base.Offset; + + public OffsetCorrectionClock(IClock source, BindableDouble pauseRateAdjust) + : base(source) + { + this.pauseRateAdjust = pauseRateAdjust; + } + + public override void ProcessFrame() + { + base.ProcessFrame(); + updateOffset(); + } + + private void updateOffset() + { + // changing this during the pause transform effect will cause a potentially large offset to be suddenly applied as we approach zero rate. + if (pauseRateAdjust.Value == 1) + { + // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. + base.Offset = Offset * Rate; + } + } + } +} From 43879633db8810209ffeab2ed1f7d8f9f36a06ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 18:28:55 +0900 Subject: [PATCH 221/532] Ensure setting a `StartTime` on a `GameplayClockContainer` always resets to the new time --- .../Gameplay/TestSceneStoryboardSamples.cs | 3 +++ osu.Game/Screens/Play/GameplayClockContainer.cs | 13 ++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 18ae2cb7c8..afdcedb485 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -90,6 +90,9 @@ namespace osu.Game.Tests.Gameplay AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue); } + /// + /// Sample at 0ms, start time at 1000ms (so the sample should not be played). + /// [Test] public void TestSampleHasLifetimeEndWithInitialClockTime() { diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 33f4f26dfb..ae9779434d 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -38,12 +38,23 @@ namespace osu.Game.Screens.Play /// /// The time from which the clock should start. Will be seeked to on calling . + /// Settting a start time will to the new value. /// /// /// By default, a value of zero will be used. /// Importantly, the value will be inferred from the current beatmap in by default. /// - public double StartTime { get; set; } = 0; + public double StartTime + { + get => startTime; + set + { + startTime = value; + Reset(); + } + } + + private double startTime; public virtual IEnumerable NonGameplayAdjustments => Enumerable.Empty(); From 2eba8650cae909516b1a38c97c249d82cb25acbc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 16:32:43 +0900 Subject: [PATCH 222/532] Update `TestSceneLeadIn` to use new assert style --- .../Visual/Gameplay/TestSceneLeadIn.cs | 31 +++++-------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index 0d80d29cab..25251bf1d6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -19,7 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneLeadIn : RateAdjustedBeatmapTestScene { - private LeadInPlayer player; + private LeadInPlayer player = null!; private const double lenience_ms = 10; @@ -36,11 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay BeatmapInfo = { AudioLeadIn = leadIn } }); - AddStep("check first frame time", () => - { - Assert.That(player.FirstFrameClockTime, Is.Not.Null); - Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms)); - }); + checkFirstFrameTime(expectedStartTime); } [TestCase(1000, 0)] @@ -59,11 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard); - AddStep("check first frame time", () => - { - Assert.That(player.FirstFrameClockTime, Is.Not.Null); - Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms)); - }); + checkFirstFrameTime(expectedStartTime); } [TestCase(1000, 0, false)] @@ -97,14 +87,13 @@ namespace osu.Game.Tests.Visual.Gameplay loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard); - AddStep("check first frame time", () => - { - Assert.That(player.FirstFrameClockTime, Is.Not.Null); - Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms)); - }); + checkFirstFrameTime(expectedStartTime); } - private void loadPlayerWithBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + private void checkFirstFrameTime(double expectedStartTime) => + AddAssert("check first frame time", () => player.FirstFrameClockTime, () => Is.EqualTo(expectedStartTime).Within(lenience_ms)); + + private void loadPlayerWithBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) { AddStep("create player", () => { @@ -126,12 +115,8 @@ namespace osu.Game.Tests.Visual.Gameplay public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; - public double GameplayStartTime => DrawableRuleset.GameplayStartTime; - public double FirstHitObjectTime => DrawableRuleset.Objects.First().StartTime; - public double GameplayClockTime => GameplayClockContainer.CurrentTime; - protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); From cc86909633782c406521d11840c9974fc7f8ab33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 18:55:36 +0900 Subject: [PATCH 223/532] Increase lenience on `TestSceneLeadIn` tests I've gone through these in detail and can't find an issue with the actual flow of things. For whatever reason, the new structure has a slightly higher delay, likely due to performing less `Seek` calls (previously a `Seek` was called after the clock start which may have been making this more accurate on the first `Player.Update`). I don't think it really matters that this is slightly off, but we'll see how this plays out. --- osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index 25251bf1d6..c066f1417c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Gameplay { private LeadInPlayer player = null!; - private const double lenience_ms = 10; + private const double lenience_ms = 100; private const double first_hit_object = 2170; From 0e228791c0c8353c0a277b9bb2b0aa7e8b5a41be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 19:02:13 +0900 Subject: [PATCH 224/532] Remove unnecessary `Reset` call in `MultiSpectatorScreen` --- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 7ed0be50e5..f200702e80 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -160,8 +160,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { base.LoadComplete(); - masterClockContainer.Reset(); - syncManager.ReadyToStart += onReadyToStart; syncManager.MasterState.BindValueChanged(onMasterStateChanged, true); } From 7bc96431a782870ab9951edd63f194951bd942f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 19:08:23 +0900 Subject: [PATCH 225/532] Remove unnecessary `virtual` spec from `GameplayClockContainer.Seek` --- osu.Game/Screens/Play/GameplayClockContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index ae9779434d..6c900a727f 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.Play /// Seek to a specific time in gameplay. /// /// The destination time to seek to. - public virtual void Seek(double time) + public void Seek(double time) { Logger.Log($"{nameof(GameplayClockContainer)} seeking to {time}"); From 1d774f3f12b1858275ae0d19608f7cdd872a65d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 19:12:49 +0900 Subject: [PATCH 226/532] Remove redundant `ProcessFrame` calls Of note, I'm not sure whether the `IsPaused` check was meaningful, but it's not reimplemented in the new `FramedBeatmapClock`. --- osu.Game/Screens/Play/GameplayClockContainer.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 6c900a727f..8770c58430 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -117,9 +117,6 @@ namespace osu.Game.Screens.Play GameplayClock.Seek(time); - // Manually process to make sure the gameplay clock is correctly updated after a seek. - GameplayClock.ProcessFrame(); - OnSeek?.Invoke(); } @@ -163,14 +160,6 @@ namespace osu.Game.Screens.Play ChangeSource(SourceClock); } - protected override void Update() - { - if (!IsPaused.Value) - GameplayClock.ProcessFrame(); - - base.Update(); - } - /// /// Invoked when the value of is changed to start or stop the clock. /// From 15d49b0357f444fde1079a4eb095f5b1d9a8a5cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Aug 2022 01:19:24 +0900 Subject: [PATCH 227/532] Update `TestSceneSpectator` to user new assert style --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 0aa412a4fd..929af7f84d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(startTime: gameplay_start); - AddAssert("time is greater than seek target", () => currentFrameStableTime > gameplay_start); + AddAssert("time is greater than seek target", () => currentFrameStableTime, () => Is.GreaterThan(gameplay_start)); } /// @@ -119,7 +119,7 @@ namespace osu.Game.Tests.Visual.Gameplay waitForPlayer(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); - AddAssert("time is greater than seek target", () => currentFrameStableTime > gameplay_start); + AddAssert("time is greater than seek target", () => currentFrameStableTime, () => Is.GreaterThan(gameplay_start)); } [Test] @@ -147,7 +147,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for frame starvation", () => replayHandler.WaitingForFrame); checkPaused(true); - AddAssert("time advanced", () => currentFrameStableTime > pausedTime); + AddAssert("time advanced", () => currentFrameStableTime, () => Is.GreaterThan(pausedTime)); } [Test] @@ -176,7 +176,7 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(300); - AddUntilStep("playing from correct point in time", () => player.ChildrenOfType().First().FrameStableClock.CurrentTime > 30000); + AddUntilStep("playing from correct point in time", () => player.ChildrenOfType().First().FrameStableClock.CurrentTime, () => Is.GreaterThan(30000)); } [Test] From 3eb1cda6aab101637516a5027311c3264858f3dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Aug 2022 01:40:12 +0900 Subject: [PATCH 228/532] Reorganise call order of `Start` / `Reset` to make more sense --- osu.Game/Screens/Play/GameplayClockContainer.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 8770c58430..9f13d15890 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -89,22 +89,22 @@ namespace osu.Game.Screens.Play } /// - /// Starts gameplay. + /// Starts gameplay and marks un-paused state. /// public virtual void Start() { ensureSourceClockSet(); + isPaused.Value = false; + + // the clock may be stopped via internal means (ie. not via `IsPaused`). if (!GameplayClock.IsRunning) { // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time // This accounts for the clock source potentially taking time to enter a completely stopped state Seek(GameplayClock.CurrentTime); - GameplayClock.Start(); } - - isPaused.Value = false; } /// @@ -121,7 +121,7 @@ namespace osu.Game.Screens.Play } /// - /// Stops gameplay. + /// Stops gameplay and marks paused state. /// public void Stop() => isPaused.Value = true; @@ -134,11 +134,11 @@ namespace osu.Game.Screens.Play // Manually stop the source in order to not affect the IsPaused state. GameplayClock.Stop(); - if (!IsPaused.Value || startClock) - Start(); - ensureSourceClockSet(); Seek(StartTime); + + if (!IsPaused.Value || startClock) + Start(); } /// From 4c24d8ed58ff64f69f474f9f38d45a3e7372bb2d Mon Sep 17 00:00:00 2001 From: its5Q Date: Fri, 19 Aug 2022 03:17:05 +1000 Subject: [PATCH 229/532] Improve string consistency --- osu.Game/Localisation/EditorSetupStrings.cs | 8 ++++---- osu.Game/Screens/Edit/Setup/ColoursSection.cs | 2 +- osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Localisation/EditorSetupStrings.cs b/osu.Game/Localisation/EditorSetupStrings.cs index 9512f3ff14..0431b9cf76 100644 --- a/osu.Game/Localisation/EditorSetupStrings.cs +++ b/osu.Game/Localisation/EditorSetupStrings.cs @@ -10,9 +10,9 @@ namespace osu.Game.Localisation private const string prefix = @"osu.Game.Resources.Localisation.EditorSetup"; /// - /// "beatmap setup" + /// "Beatmap Setup" /// - public static LocalisableString BeatmapSetup => new TranslatableString(getKey(@"beatmap_setup"), @"beatmap setup"); + public static LocalisableString BeatmapSetup => new TranslatableString(getKey(@"beatmap_setup"), @"Beatmap Setup"); /// /// "change general settings of your beatmap" @@ -25,9 +25,9 @@ namespace osu.Game.Localisation public static LocalisableString ColoursHeader => new TranslatableString(getKey(@"colours_header"), @"Colours"); /// - /// "Hitcircle / Slider Combos" + /// "Hit circle / Slider Combos" /// - public static LocalisableString HitcircleSliderCombos => new TranslatableString(getKey(@"hitcircle_slider_combos"), @"Hitcircle / Slider Combos"); + public static LocalisableString HitCircleSliderCombos => new TranslatableString(getKey(@"hit_circle_slider_combos"), @"Hit circle / Slider Combos"); /// /// "Design" diff --git a/osu.Game/Screens/Edit/Setup/ColoursSection.cs b/osu.Game/Screens/Edit/Setup/ColoursSection.cs index 9334967a3e..e3fcdedd1b 100644 --- a/osu.Game/Screens/Edit/Setup/ColoursSection.cs +++ b/osu.Game/Screens/Edit/Setup/ColoursSection.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Edit.Setup { comboColours = new LabelledColourPalette { - Label = EditorSetupStrings.HitcircleSliderCombos, + Label = EditorSetupStrings.HitCircleSliderCombos, FixedLabelWidth = LABEL_WIDTH, ColourNamePrefix = EditorSetupStrings.ComboColourPrefix } diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs index c1f6ab556c..9486b3728b 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs @@ -4,6 +4,7 @@ #nullable disable using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -78,7 +79,7 @@ namespace osu.Game.Screens.Edit.Setup { public SetupScreenTitle() { - Title = EditorSetupStrings.BeatmapSetup; + Title = EditorSetupStrings.BeatmapSetup.ToLower(); Description = EditorSetupStrings.BeatmapSetupDescription; IconTexture = "Icons/Hexacons/social"; } From 89eb0a4079c4b5564e47623008ff81067a2e4a08 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 19 Aug 2022 01:10:54 +0200 Subject: [PATCH 230/532] Added TestScene for slider splitting --- .../Editor/TestSceneSliderSplitting.cs | 127 ++++++++++++++++++ .../Sliders/SliderSelectionBlueprint.cs | 8 +- 2 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs new file mode 100644 index 0000000000..4693d56789 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -0,0 +1,127 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public class TestSceneSliderSplitting : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + private ComposeBlueprintContainer blueprintContainer + => Editor.ChildrenOfType().First(); + + private ContextMenuContainer contextMenuContainer + => Editor.ChildrenOfType().First(); + + private Slider? slider; + private PathControlPointVisualiser? visualiser; + + [Test] + public void TestBasicSplit() + { + AddStep("add slider", () => + { + slider = new Slider + { + Position = new Vector2(0, 50), + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(new Vector2(150, 150)), + new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(400, 0)), + new PathControlPoint(new Vector2(400, 150)) + }) + }; + + EditorBeatmap.Add(slider); + }); + + AddStep("select added slider", () => + { + EditorBeatmap.SelectedHitObjects.Add(slider); + visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType().First(); + }); + + moveMouseToControlPoint(2); + AddStep("select control point", () => + { + if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true; + }); + addContextMenuItemStep("Split control point"); + } + + [Test] + public void TestStartTimeOffsetPlusDeselect() + { + HitCircle? circle = null; + + AddStep("add circle", () => + { + circle = new HitCircle(); + + EditorBeatmap.Add(circle); + }); + + AddStep("select added circle", () => + { + EditorBeatmap.SelectedHitObjects.Add(circle); + }); + + AddStep("add another circle", () => + { + var circle2 = new HitCircle(); + + EditorBeatmap.Add(circle2); + }); + + AddStep("change time of selected circle and deselect", () => + { + if (circle is null) return; + + circle.StartTime += 1; + EditorBeatmap.SelectedHitObjects.Clear(); + }); + } + + private void moveMouseToControlPoint(int index) + { + AddStep($"move mouse to control point {index}", () => + { + if (slider is null || visualiser is null) return; + + Vector2 position = slider.Path.ControlPoints[index].Position + slider.Position; + InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position)); + }); + } + + private void addContextMenuItemStep(string contextMenuText) + { + AddStep($"click context menu item \"{contextMenuText}\"", () => + { + if (visualiser is null) return; + + MenuItem? item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); + + item?.Action?.Value(); + }); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 70993ef7ac..e28dbb485d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -294,11 +294,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders Path = new SliderPath(splitControlPoints.Select(o => new PathControlPoint(o.Position - splitControlPoints[0].Position, o == splitControlPoints[^1] ? null : o.Type)).ToArray()) }; + HitObject.StartTime += 1; + editorBeatmap.Add(newSlider); HitObject.NewCombo = false; HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance; - HitObject.StartTime += newSlider.SpanDuration; + HitObject.StartTime += newSlider.SpanDuration - 1; // In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider. if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON) @@ -307,14 +309,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - editorBeatmap.SelectedHitObjects.Clear(); - // The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position // So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0) Vector2 first = controlPoints[0].Position; foreach (var c in controlPoints) c.Position -= first; HitObject.Position += first; + + editorBeatmap.SelectedHitObjects.Clear(); } private void convertToStream() From 41408a3106215074c6c2d71944b5efa7790ebaf1 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 19 Aug 2022 15:51:27 +0900 Subject: [PATCH 231/532] Add audio feedback for text selection --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 1650d2b274..2718465b9c 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -46,8 +46,16 @@ namespace osu.Game.Graphics.UserInterface private Sample? textCommittedSample; private Sample? caretMovedSample; + private Sample? selectCharSample; + private Sample? selectWordSample; + private Sample? selectAllSample; + private Sample? deselectSample; + private OsuCaret? caret; + private bool selectionStarted; + private double sampleLastPlaybackTime; + public OsuTextBox() { Height = 40; @@ -78,6 +86,11 @@ namespace osu.Game.Graphics.UserInterface textRemovedSample = audio.Samples.Get(@"Keyboard/key-delete"); textCommittedSample = audio.Samples.Get(@"Keyboard/key-confirm"); caretMovedSample = audio.Samples.Get(@"Keyboard/key-movement"); + + selectCharSample = audio.Samples.Get(@"Keyboard/select-char"); + selectWordSample = audio.Samples.Get(@"Keyboard/select-word"); + selectAllSample = audio.Samples.Get(@"Keyboard/select-all"); + deselectSample = audio.Samples.Get(@"Keyboard/deselect"); } private Color4 selectionColour; @@ -112,7 +125,72 @@ namespace osu.Game.Graphics.UserInterface { base.OnCaretMoved(selecting); - caretMovedSample?.Play(); + if (!selecting) + caretMovedSample?.Play(); + } + + protected override void OnTextSelectionChanged(TextSelectionType selectionType) + { + base.OnTextSelectionChanged(selectionType); + + if (selectionType == TextSelectionType.Word) + { + if (!selectionStarted) + playSelectSample(selectionType); + else + playSelectSample(); + } + else + { + playSelectSample(selectionType); + } + + selectionStarted = true; + } + + protected override void OnTextDeselected() + { + if (selectionStarted) + playSelectSample(TextSelectionType.Deselect); + + selectionStarted = false; + + base.OnTextDeselected(); + } + + private void playSelectSample(TextSelectionType selectionType = TextSelectionType.Character) + { + if (Time.Current < sampleLastPlaybackTime + 15) return; + + SampleChannel? channel; + double pitch = 0.98 + RNG.NextDouble(0.04); + + switch (selectionType) + { + case TextSelectionType.All: + channel = selectAllSample?.GetChannel(); + break; + + case TextSelectionType.Word: + channel = selectWordSample?.GetChannel(); + break; + + case TextSelectionType.Deselect: + channel = deselectSample?.GetChannel(); + break; + + default: + channel = selectCharSample?.GetChannel(); + pitch += (SelectedText.Length / (float)Text.Length) * 0.15f; + break; + } + + if (channel == null) return; + + channel.Frequency.Value = pitch; + channel.Play(); + + sampleLastPlaybackTime = Time.Current; } protected override void OnImeComposition(string newComposition, int removedTextLength, int addedTextLength, bool caretMoved) From 5dcd4ce7c52de316d3b2f8665ffccfc86c3b9ada Mon Sep 17 00:00:00 2001 From: vun Date: Fri, 19 Aug 2022 15:31:03 +0800 Subject: [PATCH 232/532] Naming changes --- .../Difficulty/Evaluators/ColourEvaluator.cs | 12 ++--- ...rEncoding.cs => AlternatingMonoPattern.cs} | 22 ++++----- .../Data/{MonoEncoding.cs => MonoStreak.cs} | 10 ++-- ...lourEncoding.cs => RepeatingHitPattern.cs} | 28 +++++------ .../TaikoColourDifficultyPreprocessor.cs | 46 +++++++++---------- .../Colour/TaikoDifficultyHitObjectColour.cs | 6 +-- 6 files changed, 62 insertions(+), 62 deletions(-) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/{ColourEncoding.cs => AlternatingMonoPattern.cs} (54%) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/{MonoEncoding.cs => MonoStreak.cs} (81%) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/{CoupledColourEncoding.cs => RepeatingHitPattern.cs} (59%) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 30094dc869..912a02f30e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -26,25 +26,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators } /// - /// Evaluate the difficulty of the first note of a . + /// Evaluate the difficulty of the first note of a . /// - public static double EvaluateDifficultyOf(MonoEncoding encoding) + public static double EvaluateDifficultyOf(MonoStreak encoding) { return sigmoid(encoding.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(encoding.Parent!) * 0.5; } /// - /// Evaluate the difficulty of the first note of a . + /// Evaluate the difficulty of the first note of a . /// - public static double EvaluateDifficultyOf(ColourEncoding encoding) + public static double EvaluateDifficultyOf(AlternatingMonoPattern encoding) { return sigmoid(encoding.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(encoding.Parent!); } /// - /// Evaluate the difficulty of the first note of a . + /// Evaluate the difficulty of the first note of a . /// - public static double EvaluateDifficultyOf(CoupledColourEncoding encoding) + public static double EvaluateDifficultyOf(RepeatingHitPatterns encoding) { return 2 * (1 - sigmoid(encoding.RepetitionInterval, 2, 2, 0.5, 1)); } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs similarity index 54% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs index cd39a3d232..bb4ddc73d0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/ColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs @@ -7,20 +7,20 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data { /// - /// Encodes a list of s. - /// s with the same are grouped together. + /// Encodes a list of s. + /// s with the same are grouped together. /// - public class ColourEncoding + public class AlternatingMonoPattern { /// - /// s that are grouped together within this . + /// s that are grouped together within this . /// - public readonly List Payload = new List(); + public readonly List Payload = new List(); /// - /// The parent that contains this + /// The parent that contains this /// - public CoupledColourEncoding? Parent; + public RepeatingHitPatterns? Parent; /// /// Index of this encoding within it's parent encoding @@ -28,10 +28,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data public int Index; /// - /// Determine if this is a repetition of another . This + /// Determine if this is a repetition of another . This /// is a strict comparison and is true if and only if the colour sequence is exactly the same. /// - public bool IsRepetitionOf(ColourEncoding other) + public bool IsRepetitionOf(AlternatingMonoPattern other) { return HasIdenticalMonoLength(other) && other.Payload.Count == Payload.Count && @@ -40,9 +40,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data } /// - /// Determine if this has the same mono length of another . + /// Determine if this has the same mono length of another . /// - public bool HasIdenticalMonoLength(ColourEncoding other) + public bool HasIdenticalMonoLength(AlternatingMonoPattern other) { return other.Payload[0].RunLength == Payload[0].RunLength; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs similarity index 81% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs index 0e998696f9..26175d9559 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs @@ -9,19 +9,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data { /// /// Encode colour information for a sequence of s. Consecutive s - /// of the same are encoded within the same . + /// of the same are encoded within the same . /// - public class MonoEncoding + public class MonoStreak { /// - /// List of s that are encoded within this . + /// List of s that are encoded within this . /// public List EncodedData { get; private set; } = new List(); /// - /// The parent that contains this + /// The parent that contains this /// - public ColourEncoding? Parent; + public AlternatingMonoPattern? Parent; /// /// Index of this encoding within it's parent encoding diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPattern.cs similarity index 59% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPattern.cs index 3f692e9d3d..91b41b80e7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/CoupledColourEncoding.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPattern.cs @@ -7,33 +7,33 @@ using System.Collections.Generic; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data { /// - /// Encodes a list of s, grouped together by back and forth repetition of the same - /// . Also stores the repetition interval between this and the previous . + /// Encodes a list of s, grouped together by back and forth repetition of the same + /// . Also stores the repetition interval between this and the previous . /// - public class CoupledColourEncoding + public class RepeatingHitPatterns { /// - /// Maximum amount of s to look back to find a repetition. + /// Maximum amount of s to look back to find a repetition. /// private const int max_repetition_interval = 16; /// - /// The s that are grouped together within this . + /// The s that are grouped together within this . /// - public readonly List Payload = new List(); + public readonly List Payload = new List(); /// - /// The previous . This is used to determine the repetition interval. + /// The previous . This is used to determine the repetition interval. /// - public readonly CoupledColourEncoding? Previous; + public readonly RepeatingHitPatterns? Previous; /// - /// How many between the current and previous identical . + /// How many between the current and previous identical . /// If no repetition is found this will have a value of + 1. /// public int RepetitionInterval { get; private set; } = max_repetition_interval + 1; - public CoupledColourEncoding(CoupledColourEncoding? previous) + public RepeatingHitPatterns(RepeatingHitPatterns? previous) { Previous = previous; } @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// Returns true if other is considered a repetition of this encoding. This is true if other's first two payloads /// have identical mono lengths. /// - private bool isRepetitionOf(CoupledColourEncoding other) + private bool isRepetitionOf(RepeatingHitPatterns other) { if (Payload.Count != other.Payload.Count) return false; @@ -55,8 +55,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data } /// - /// Finds the closest previous that has the identical . - /// Interval is defined as the amount of chunks between the current and repeated encoding. + /// Finds the closest previous that has the identical . + /// Interval is defined as the amount of chunks between the current and repeated encoding. /// public void FindRepetitionInterval() { @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data return; } - CoupledColourEncoding? other = Previous; + RepeatingHitPatterns? other = Previous; int interval = 1; while (interval < max_repetition_interval) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index 81ba219bc0..5ae659574d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// public static void ProcessAndAssign(List hitObjects) { - List encodings = encode(hitObjects); + List encodings = encode(hitObjects); // Assign indexing and encoding data to all relevant objects. Only the first note of each encoding type is // assigned with the relevant encodings. @@ -33,14 +33,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour // documentation. for (int i = 0; i < coupledEncoding.Payload.Count; ++i) { - ColourEncoding colourEncoding = coupledEncoding.Payload[i]; + AlternatingMonoPattern colourEncoding = coupledEncoding.Payload[i]; colourEncoding.Parent = coupledEncoding; colourEncoding.Index = i; colourEncoding.Payload[0].EncodedData[0].Colour.ColourEncoding = colourEncoding; for (int j = 0; j < colourEncoding.Payload.Count; ++j) { - MonoEncoding monoEncoding = colourEncoding.Payload[j]; + MonoStreak monoEncoding = colourEncoding.Payload[j]; monoEncoding.Parent = colourEncoding; monoEncoding.Index = j; monoEncoding.EncodedData[0].Colour.MonoEncoding = monoEncoding; @@ -50,24 +50,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour } /// - /// Encodes a list of s into a list of s. + /// Encodes a list of s into a list of s. /// - private static List encode(List data) + private static List encode(List data) { - List firstPass = encodeMono(data); - List secondPass = encodeColour(firstPass); - List thirdPass = encodeCoupledColour(secondPass); + List firstPass = encodeMono(data); + List secondPass = encodeColour(firstPass); + List thirdPass = encodeCoupledColour(secondPass); return thirdPass; } /// - /// Encodes a list of s into a list of s. + /// Encodes a list of s into a list of s. /// - private static List encodeMono(List data) + private static List encodeMono(List data) { - List encodings = new List(); - MonoEncoding? currentEncoding = null; + List encodings = new List(); + MonoStreak? currentEncoding = null; for (int i = 0; i < data.Count; i++) { @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour // If this is the first object in the list or the colour changed, create a new mono encoding if (currentEncoding == null || previousObject == null || (taikoObject.BaseObject as Hit)?.Type != (previousObject.BaseObject as Hit)?.Type) { - currentEncoding = new MonoEncoding(); + currentEncoding = new MonoStreak(); encodings.Add(currentEncoding); } @@ -91,19 +91,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour } /// - /// Encodes a list of s into a list of s. + /// Encodes a list of s into a list of s. /// - private static List encodeColour(List data) + private static List encodeColour(List data) { - List encodings = new List(); - ColourEncoding? currentEncoding = null; + List encodings = new List(); + AlternatingMonoPattern? currentEncoding = null; for (int i = 0; i < data.Count; i++) { // Start a new ColourEncoding if the previous MonoEncoding has a different mono length, or if this is the first MonoEncoding in the list. if (currentEncoding == null || data[i].RunLength != data[i - 1].RunLength) { - currentEncoding = new ColourEncoding(); + currentEncoding = new AlternatingMonoPattern(); encodings.Add(currentEncoding); } @@ -115,17 +115,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour } /// - /// Encodes a list of s into a list of s. + /// Encodes a list of s into a list of s. /// - private static List encodeCoupledColour(List data) + private static List encodeCoupledColour(List data) { - List encodings = new List(); - CoupledColourEncoding? currentEncoding = null; + List encodings = new List(); + RepeatingHitPatterns? currentEncoding = null; for (int i = 0; i < data.Count; i++) { // Start a new CoupledColourEncoding. ColourEncodings that should be grouped together will be handled later within this loop. - currentEncoding = new CoupledColourEncoding(currentEncoding); + currentEncoding = new RepeatingHitPatterns(currentEncoding); // Determine if future ColourEncodings should be grouped. bool isCoupled = i < data.Count - 2 && data[i].IsRepetitionOf(data[i + 2]); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs index 41080eeb33..708ce8ecd0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs @@ -13,16 +13,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// /// The that encodes this note, only present if this is the first note within a /// - public MonoEncoding? MonoEncoding; + public MonoStreak? MonoEncoding; /// /// The that encodes this note, only present if this is the first note within a /// - public ColourEncoding? ColourEncoding; + public AlternatingMonoPattern? ColourEncoding; /// /// The that encodes this note, only present if this is the first note within a /// - public CoupledColourEncoding? CoupledColourEncoding; + public RepeatingHitPatterns? CoupledColourEncoding; } } From 51176e95772736807b05d0158f8a79d14018dada Mon Sep 17 00:00:00 2001 From: vun Date: Fri, 19 Aug 2022 15:45:43 +0800 Subject: [PATCH 233/532] Naming changes --- .../Difficulty/Evaluators/ColourEvaluator.cs | 24 ++-- .../Colour/Data/AlternatingMonoPattern.cs | 12 +- .../Preprocessing/Colour/Data/MonoStreak.cs | 6 +- .../Colour/Data/RepeatingHitPattern.cs | 14 +-- .../TaikoColourDifficultyPreprocessor.cs | 108 +++++++++--------- .../Colour/TaikoDifficultyHitObjectColour.cs | 12 +- .../Difficulty/Skills/Colour.cs | 4 +- 7 files changed, 90 insertions(+), 90 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 912a02f30e..afddedf962 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -28,25 +28,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// /// Evaluate the difficulty of the first note of a . /// - public static double EvaluateDifficultyOf(MonoStreak encoding) + public static double EvaluateDifficultyOf(MonoStreak monoStreak) { - return sigmoid(encoding.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(encoding.Parent!) * 0.5; + return sigmoid(monoStreak.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(monoStreak.Parent!) * 0.5; } /// /// Evaluate the difficulty of the first note of a . /// - public static double EvaluateDifficultyOf(AlternatingMonoPattern encoding) + public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern) { - return sigmoid(encoding.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(encoding.Parent!); + return sigmoid(alternatingMonoPattern.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(alternatingMonoPattern.Parent!); } /// /// Evaluate the difficulty of the first note of a . /// - public static double EvaluateDifficultyOf(RepeatingHitPatterns encoding) + public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern) { - return 2 * (1 - sigmoid(encoding.RepetitionInterval, 2, 2, 0.5, 1)); + return 2 * (1 - sigmoid(repeatingHitPattern.RepetitionInterval, 2, 2, 0.5, 1)); } public static double EvaluateDifficultyOf(DifficultyHitObject hitObject) @@ -54,12 +54,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour; double difficulty = 0.0d; - if (colour.MonoEncoding != null) // Difficulty for MonoEncoding - difficulty += EvaluateDifficultyOf(colour.MonoEncoding); - if (colour.ColourEncoding != null) // Difficulty for ColourEncoding - difficulty += EvaluateDifficultyOf(colour.ColourEncoding); - if (colour.CoupledColourEncoding != null) // Difficulty for CoupledColourEncoding - difficulty += EvaluateDifficultyOf(colour.CoupledColourEncoding); + if (colour.MonoStreak != null) // Difficulty for MonoStreak + difficulty += EvaluateDifficultyOf(colour.MonoStreak); + if (colour.AlternatingMonoPattern != null) // Difficulty for AlternatingMonoPattern + difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern); + if (colour.RepeatingHitPatterns != null) // Difficulty for RepeatingHitPattern + difficulty += EvaluateDifficultyOf(colour.RepeatingHitPatterns); return difficulty; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs index bb4ddc73d0..9d2df877d3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// /// s that are grouped together within this . /// - public readonly List Payload = new List(); + public readonly List MonoStreaks = new List(); /// /// The parent that contains this @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data public RepeatingHitPatterns? Parent; /// - /// Index of this encoding within it's parent encoding + /// Index of this within it's parent /// public int Index; @@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data public bool IsRepetitionOf(AlternatingMonoPattern other) { return HasIdenticalMonoLength(other) && - other.Payload.Count == Payload.Count && - (other.Payload[0].EncodedData[0].BaseObject as Hit)?.Type == - (Payload[0].EncodedData[0].BaseObject as Hit)?.Type; + other.MonoStreaks.Count == MonoStreaks.Count && + (other.MonoStreaks[0].HitObjects[0].BaseObject as Hit)?.Type == + (MonoStreaks[0].HitObjects[0].BaseObject as Hit)?.Type; } /// @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// public bool HasIdenticalMonoLength(AlternatingMonoPattern other) { - return other.Payload[0].RunLength == Payload[0].RunLength; + return other.MonoStreaks[0].RunLength == MonoStreaks[0].RunLength; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs index 26175d9559..82a09d61fe 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// /// List of s that are encoded within this . /// - public List EncodedData { get; private set; } = new List(); + public List HitObjects { get; private set; } = new List(); /// /// The parent that contains this @@ -24,13 +24,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data public AlternatingMonoPattern? Parent; /// - /// Index of this encoding within it's parent encoding + /// Index of this within it's parent /// public int Index; /// /// How long the mono pattern encoded within is /// - public int RunLength => EncodedData.Count; + public int RunLength => HitObjects.Count; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPattern.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPattern.cs index 91b41b80e7..d5ce2d0a55 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPattern.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPattern.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// /// The s that are grouped together within this . /// - public readonly List Payload = new List(); + public readonly List AlternatingMonoPatterns = new List(); /// /// The previous . This is used to determine the repetition interval. @@ -39,24 +39,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data } /// - /// Returns true if other is considered a repetition of this encoding. This is true if other's first two payloads + /// Returns true if other is considered a repetition of this pattern. This is true if other's first two payloads /// have identical mono lengths. /// private bool isRepetitionOf(RepeatingHitPatterns other) { - if (Payload.Count != other.Payload.Count) return false; + if (AlternatingMonoPatterns.Count != other.AlternatingMonoPatterns.Count) return false; - for (int i = 0; i < Math.Min(Payload.Count, 2); i++) + for (int i = 0; i < Math.Min(AlternatingMonoPatterns.Count, 2); i++) { - if (!Payload[i].HasIdenticalMonoLength(other.Payload[i])) return false; + if (!AlternatingMonoPatterns[i].HasIdenticalMonoLength(other.AlternatingMonoPatterns[i])) return false; } return true; } /// - /// Finds the closest previous that has the identical . - /// Interval is defined as the amount of chunks between the current and repeated encoding. + /// Finds the closest previous that has the identical . + /// Interval is defined as the amount of chunks between the current and repeated patterns. /// public void FindRepetitionInterval() { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index 5ae659574d..bd46957fc0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -20,30 +20,30 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// public static void ProcessAndAssign(List hitObjects) { - List encodings = encode(hitObjects); + List hitPatterns = encode(hitObjects); // Assign indexing and encoding data to all relevant objects. Only the first note of each encoding type is // assigned with the relevant encodings. - foreach (var coupledEncoding in encodings) + foreach (var repeatingHitPattern in hitPatterns) { - coupledEncoding.Payload[0].Payload[0].EncodedData[0].Colour.CoupledColourEncoding = coupledEncoding; + repeatingHitPattern.AlternatingMonoPatterns[0].MonoStreaks[0].HitObjects[0].Colour.RepeatingHitPatterns = repeatingHitPattern; // The outermost loop is kept a ForEach loop since it doesn't need index information, and we want to - // keep i and j for ColourEncoding's and MonoEncoding's index respectively, to keep it in line with + // keep i and j for AlternatingMonoPattern's and MonoStreak's index respectively, to keep it in line with // documentation. - for (int i = 0; i < coupledEncoding.Payload.Count; ++i) + for (int i = 0; i < repeatingHitPattern.AlternatingMonoPatterns.Count; ++i) { - AlternatingMonoPattern colourEncoding = coupledEncoding.Payload[i]; - colourEncoding.Parent = coupledEncoding; - colourEncoding.Index = i; - colourEncoding.Payload[0].EncodedData[0].Colour.ColourEncoding = colourEncoding; + AlternatingMonoPattern monoPattern = repeatingHitPattern.AlternatingMonoPatterns[i]; + monoPattern.Parent = repeatingHitPattern; + monoPattern.Index = i; + monoPattern.MonoStreaks[0].HitObjects[0].Colour.AlternatingMonoPattern = monoPattern; - for (int j = 0; j < colourEncoding.Payload.Count; ++j) + for (int j = 0; j < monoPattern.MonoStreaks.Count; ++j) { - MonoStreak monoEncoding = colourEncoding.Payload[j]; - monoEncoding.Parent = colourEncoding; - monoEncoding.Index = j; - monoEncoding.EncodedData[0].Colour.MonoEncoding = monoEncoding; + MonoStreak monoStreak = monoPattern.MonoStreaks[j]; + monoStreak.Parent = monoPattern; + monoStreak.Index = j; + monoStreak.HitObjects[0].Colour.MonoStreak = monoStreak; } } } @@ -54,20 +54,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour /// private static List encode(List data) { - List firstPass = encodeMono(data); - List secondPass = encodeColour(firstPass); - List thirdPass = encodeCoupledColour(secondPass); + List monoStreaks = encodeMonoStreak(data); + List alternatingMonoPatterns = encodeAlternatingMonoPattern(monoStreaks); + List repeatingHitPatterns = encodeRepeatingHitPattern(alternatingMonoPatterns); - return thirdPass; + return repeatingHitPatterns; } /// /// Encodes a list of s into a list of s. /// - private static List encodeMono(List data) + private static List encodeMonoStreak(List data) { - List encodings = new List(); - MonoStreak? currentEncoding = null; + List monoStreaks = new List(); + MonoStreak? currentMonoStreak = null; for (int i = 0; i < data.Count; i++) { @@ -76,92 +76,92 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour // This ignores all non-note objects, which may or may not be the desired behaviour TaikoDifficultyHitObject? previousObject = taikoObject.PreviousNote(0); - // If this is the first object in the list or the colour changed, create a new mono encoding - if (currentEncoding == null || previousObject == null || (taikoObject.BaseObject as Hit)?.Type != (previousObject.BaseObject as Hit)?.Type) + // If this is the first object in the list or the colour changed, create a new mono streak + if (currentMonoStreak == null || previousObject == null || (taikoObject.BaseObject as Hit)?.Type != (previousObject.BaseObject as Hit)?.Type) { - currentEncoding = new MonoStreak(); - encodings.Add(currentEncoding); + currentMonoStreak = new MonoStreak(); + monoStreaks.Add(currentMonoStreak); } // Add the current object to the encoded payload. - currentEncoding.EncodedData.Add(taikoObject); + currentMonoStreak.HitObjects.Add(taikoObject); } - return encodings; + return monoStreaks; } /// /// Encodes a list of s into a list of s. /// - private static List encodeColour(List data) + private static List encodeAlternatingMonoPattern(List data) { - List encodings = new List(); - AlternatingMonoPattern? currentEncoding = null; + List monoPatterns = new List(); + AlternatingMonoPattern? currentMonoPattern = null; for (int i = 0; i < data.Count; i++) { - // Start a new ColourEncoding if the previous MonoEncoding has a different mono length, or if this is the first MonoEncoding in the list. - if (currentEncoding == null || data[i].RunLength != data[i - 1].RunLength) + // Start a new AlternatingMonoPattern if the previous MonoStreak has a different mono length, or if this is the first MonoStreak in the list. + if (currentMonoPattern == null || data[i].RunLength != data[i - 1].RunLength) { - currentEncoding = new AlternatingMonoPattern(); - encodings.Add(currentEncoding); + currentMonoPattern = new AlternatingMonoPattern(); + monoPatterns.Add(currentMonoPattern); } - // Add the current MonoEncoding to the encoded payload. - currentEncoding.Payload.Add(data[i]); + // Add the current MonoStreak to the encoded payload. + currentMonoPattern.MonoStreaks.Add(data[i]); } - return encodings; + return monoPatterns; } /// /// Encodes a list of s into a list of s. /// - private static List encodeCoupledColour(List data) + private static List encodeRepeatingHitPattern(List data) { - List encodings = new List(); - RepeatingHitPatterns? currentEncoding = null; + List hitPatterns = new List(); + RepeatingHitPatterns? currentHitPattern = null; for (int i = 0; i < data.Count; i++) { - // Start a new CoupledColourEncoding. ColourEncodings that should be grouped together will be handled later within this loop. - currentEncoding = new RepeatingHitPatterns(currentEncoding); + // Start a new RepeatingHitPattern. AlternatingMonoPatterns that should be grouped together will be handled later within this loop. + currentHitPattern = new RepeatingHitPatterns(currentHitPattern); - // Determine if future ColourEncodings should be grouped. + // Determine if future AlternatingMonoPatterns should be grouped. bool isCoupled = i < data.Count - 2 && data[i].IsRepetitionOf(data[i + 2]); if (!isCoupled) { - // If not, add the current ColourEncoding to the encoded payload and continue. - currentEncoding.Payload.Add(data[i]); + // If not, add the current AlternatingMonoPattern to the encoded payload and continue. + currentHitPattern.AlternatingMonoPatterns.Add(data[i]); } else { - // If so, add the current ColourEncoding to the encoded payload and start repeatedly checking if the - // subsequent ColourEncodings should be grouped by increasing i and doing the appropriate isCoupled check. + // If so, add the current AlternatingMonoPattern to the encoded payload and start repeatedly checking if the + // subsequent AlternatingMonoPatterns should be grouped by increasing i and doing the appropriate isCoupled check. while (isCoupled) { - currentEncoding.Payload.Add(data[i]); + currentHitPattern.AlternatingMonoPatterns.Add(data[i]); i++; isCoupled = i < data.Count - 2 && data[i].IsRepetitionOf(data[i + 2]); } // Skip over viewed data and add the rest to the payload - currentEncoding.Payload.Add(data[i]); - currentEncoding.Payload.Add(data[i + 1]); + currentHitPattern.AlternatingMonoPatterns.Add(data[i]); + currentHitPattern.AlternatingMonoPatterns.Add(data[i + 1]); i++; } - encodings.Add(currentEncoding); + hitPatterns.Add(currentHitPattern); } // Final pass to find repetition intervals - for (int i = 0; i < encodings.Count; i++) + for (int i = 0; i < hitPatterns.Count; i++) { - encodings[i].FindRepetitionInterval(); + hitPatterns[i].FindRepetitionInterval(); } - return encodings; + return hitPatterns; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs index 708ce8ecd0..c631b8d4a8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs @@ -11,18 +11,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour public class TaikoDifficultyHitObjectColour { /// - /// The that encodes this note, only present if this is the first note within a + /// The that encodes this note, only present if this is the first note within a /// - public MonoStreak? MonoEncoding; + public MonoStreak? MonoStreak; /// - /// The that encodes this note, only present if this is the first note within a + /// The that encodes this note, only present if this is the first note within a /// - public AlternatingMonoPattern? ColourEncoding; + public AlternatingMonoPattern? AlternatingMonoPattern; /// - /// The that encodes this note, only present if this is the first note within a + /// The that encodes this note, only present if this is the first note within a /// - public RepeatingHitPatterns? CoupledColourEncoding; + public RepeatingHitPatterns? RepeatingHitPatterns; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index dac0beadda..2d45b5eed0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { protected override double SkillMultiplier => 0.12; - // This is set to decay slower than other skills, due to the fact that only the first note of each Mono/Colour/Coupled - // encoding having any difficulty values, and we want to allow colour difficulty to be able to build up even on + // This is set to decay slower than other skills, due to the fact that only the first note of each encoding class + // having any difficulty values, and we want to allow colour difficulty to be able to build up even on // slower maps. protected override double StrainDecayBase => 0.8; From a26de0a10f4f32131b658df3ffdb2c47026f027d Mon Sep 17 00:00:00 2001 From: vun Date: Fri, 19 Aug 2022 16:05:34 +0800 Subject: [PATCH 234/532] Add HitType property to MonoStreak --- .../Preprocessing/Colour/Data/AlternatingMonoPattern.cs | 3 +-- .../Difficulty/Preprocessing/Colour/Data/MonoStreak.cs | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs index 9d2df877d3..5e6f32cce2 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs @@ -35,8 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data { return HasIdenticalMonoLength(other) && other.MonoStreaks.Count == MonoStreaks.Count && - (other.MonoStreaks[0].HitObjects[0].BaseObject as Hit)?.Type == - (MonoStreaks[0].HitObjects[0].BaseObject as Hit)?.Type; + other.MonoStreaks[0].HitType == MonoStreaks[0].HitType; } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs index 82a09d61fe..9ebea156a5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data { /// /// Encode colour information for a sequence of s. Consecutive s - /// of the same are encoded within the same . + /// of the same are encoded within the same . /// public class MonoStreak { @@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// public int Index; + public HitType? HitType => (HitObjects[0].BaseObject as Hit)?.Type; + /// /// How long the mono pattern encoded within is /// From 684efefb50c16657af99b8da4e0298155b0fd314 Mon Sep 17 00:00:00 2001 From: vun Date: Fri, 19 Aug 2022 16:13:36 +0800 Subject: [PATCH 235/532] Add FirstHitObject as a property of encoding classes --- .../Difficulty/Evaluators/ColourEvaluator.cs | 4 ++-- .../Preprocessing/Colour/Data/AlternatingMonoPattern.cs | 5 +++++ .../Difficulty/Preprocessing/Colour/Data/MonoStreak.cs | 8 ++++++++ .../{RepeatingHitPattern.cs => RepeatingHitPatterns.cs} | 5 +++++ .../Colour/TaikoColourDifficultyPreprocessor.cs | 6 +++--- .../Colour/TaikoDifficultyHitObjectColour.cs | 4 ++-- 6 files changed, 25 insertions(+), 7 deletions(-) rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/{RepeatingHitPattern.cs => RepeatingHitPatterns.cs} (93%) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index afddedf962..6c685e854e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -58,8 +58,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators difficulty += EvaluateDifficultyOf(colour.MonoStreak); if (colour.AlternatingMonoPattern != null) // Difficulty for AlternatingMonoPattern difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern); - if (colour.RepeatingHitPatterns != null) // Difficulty for RepeatingHitPattern - difficulty += EvaluateDifficultyOf(colour.RepeatingHitPatterns); + if (colour.RepeatingHitPattern != null) // Difficulty for RepeatingHitPattern + difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern); return difficulty; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs index 5e6f32cce2..450eefe75c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs @@ -27,6 +27,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// public int Index; + /// + /// The first in this . + /// + public TaikoDifficultyHitObject FirstHitObject => MonoStreaks[0].FirstHitObject; + /// /// Determine if this is a repetition of another . This /// is a strict comparison and is true if and only if the colour sequence is exactly the same. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs index 9ebea156a5..4e15043acf 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs @@ -28,6 +28,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// public int Index; + /// + /// The first in this . + /// + public TaikoDifficultyHitObject FirstHitObject => HitObjects[0]; + + /// + /// The hit type of all objects encoded within this + /// public HitType? HitType => (HitObjects[0].BaseObject as Hit)?.Type; /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPattern.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPatterns.cs similarity index 93% rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPattern.cs rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPatterns.cs index d5ce2d0a55..fe0dc6dd9a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPattern.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPatterns.cs @@ -22,6 +22,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// public readonly List AlternatingMonoPatterns = new List(); + /// + /// The parent in this + /// + public TaikoDifficultyHitObject FirstHitObject => AlternatingMonoPatterns[0].FirstHitObject; + /// /// The previous . This is used to determine the repetition interval. /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs index bd46957fc0..d19e05f4e0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour // assigned with the relevant encodings. foreach (var repeatingHitPattern in hitPatterns) { - repeatingHitPattern.AlternatingMonoPatterns[0].MonoStreaks[0].HitObjects[0].Colour.RepeatingHitPatterns = repeatingHitPattern; + repeatingHitPattern.FirstHitObject.Colour.RepeatingHitPattern = repeatingHitPattern; // The outermost loop is kept a ForEach loop since it doesn't need index information, and we want to // keep i and j for AlternatingMonoPattern's and MonoStreak's index respectively, to keep it in line with @@ -36,14 +36,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour AlternatingMonoPattern monoPattern = repeatingHitPattern.AlternatingMonoPatterns[i]; monoPattern.Parent = repeatingHitPattern; monoPattern.Index = i; - monoPattern.MonoStreaks[0].HitObjects[0].Colour.AlternatingMonoPattern = monoPattern; + monoPattern.FirstHitObject.Colour.AlternatingMonoPattern = monoPattern; for (int j = 0; j < monoPattern.MonoStreaks.Count; ++j) { MonoStreak monoStreak = monoPattern.MonoStreaks[j]; monoStreak.Parent = monoPattern; monoStreak.Index = j; - monoStreak.HitObjects[0].Colour.MonoStreak = monoStreak; + monoStreak.FirstHitObject.Colour.MonoStreak = monoStreak; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs index c631b8d4a8..9c147eee9c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs @@ -21,8 +21,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour public AlternatingMonoPattern? AlternatingMonoPattern; /// - /// The that encodes this note, only present if this is the first note within a + /// The that encodes this note, only present if this is the first note within a /// - public RepeatingHitPatterns? RepeatingHitPatterns; + public RepeatingHitPatterns? RepeatingHitPattern; } } From f3e1287f04addc40f7e7ef98fe6a89d5681e5df3 Mon Sep 17 00:00:00 2001 From: vun Date: Fri, 19 Aug 2022 16:19:45 +0800 Subject: [PATCH 236/532] Remove redundant using statement --- .../Preprocessing/Colour/Data/AlternatingMonoPattern.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs index 450eefe75c..60d4e55a64 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data { From 3f0da1406562de9e432b0caee474e0685640a417 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Aug 2022 17:39:51 +0900 Subject: [PATCH 237/532] Delay start operation by one frame to allow children to see initial start time --- osu.Game/Screens/Play/GameplayClockContainer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 9f13d15890..acbbeb1e1b 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -103,7 +103,11 @@ namespace osu.Game.Screens.Play // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time // This accounts for the clock source potentially taking time to enter a completely stopped state Seek(GameplayClock.CurrentTime); - GameplayClock.Start(); + + // Delay the start operation to ensure all children components get the initial seek time. + // Without this, children may get a start time beyond StartTime without seeing the time has elapsed. + // This can manifest as events which should be fired at the precise StartTime not firing. + SchedulerAfterChildren.Add(GameplayClock.Start); } } From 426c4c9bf74412bedfc09dd9b28e43b3e0ace905 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Aug 2022 20:39:53 +0900 Subject: [PATCH 238/532] 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 6dbc6cc377..5fdf715e3a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 28452c0a74..aca0ccaf5b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 463af1143f..fa1bbd587a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - + From 41321521e57c8c996f64c14a93c178425f801f41 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Aug 2022 20:40:05 +0900 Subject: [PATCH 239/532] 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 5fdf715e3a..17a6178641 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index aca0ccaf5b..0613db891b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index fa1bbd587a..bf1e4e350c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -62,7 +62,7 @@ - + From c3c44c19cdbe96ed5305286e79ec64f0ebea9d4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Aug 2022 20:43:15 +0900 Subject: [PATCH 240/532] Use `CompositeComponent` in various locations --- osu.Game/Online/PollingComponent.cs | 2 +- .../Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs index d54b8ca75d..fcea650e2d 100644 --- a/osu.Game/Online/PollingComponent.cs +++ b/osu.Game/Online/PollingComponent.cs @@ -16,7 +16,7 @@ namespace osu.Game.Online /// /// A component which requires a constant polling process. /// - public abstract class PollingComponent : CompositeDrawable // switch away from Component because InternalChildren are used in usages. + public abstract class PollingComponent : CompositeComponent { private double? lastTimePolled; diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 2fd8445980..bb8ec4f6ff 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -27,13 +27,10 @@ namespace osu.Game.Online.Rooms /// This differs from a regular download tracking composite as this accounts for the /// databased beatmap set's checksum, to disallow from playing with an altered version of the beatmap. /// - public class OnlinePlayBeatmapAvailabilityTracker : CompositeDrawable + public class OnlinePlayBeatmapAvailabilityTracker : CompositeComponent { public readonly IBindable SelectedItem = new Bindable(); - // Required to allow child components to update. Can potentially be replaced with a `CompositeComponent` class if or when we make one. - protected override bool RequiresChildrenUpdate => true; - [Resolved] private RealmAccess realm { get; set; } = null!; From 7bf318541c90d2ea8778157167029fc60fbff233 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Aug 2022 20:57:55 +0900 Subject: [PATCH 241/532] Reword comment to hopefully read better --- osu.Game/Screens/Play/GameplayClockContainer.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index acbbeb1e1b..6dab13c950 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -104,9 +104,16 @@ namespace osu.Game.Screens.Play // This accounts for the clock source potentially taking time to enter a completely stopped state Seek(GameplayClock.CurrentTime); - // Delay the start operation to ensure all children components get the initial seek time. - // Without this, children may get a start time beyond StartTime without seeing the time has elapsed. - // This can manifest as events which should be fired at the precise StartTime not firing. + // The case which cause this to be added is FrameStabilityContainer, which manages its own current and elapsed time. + // Because we generally update our own current time quicker than children can query it (via Start/Seek/Update), + // this means that the first frame ever exposed to children may have a non-zero current time. + // + // If the child component is not aware of the parent ElapsedFrameTime (which is the case for FrameStabilityContainer) + // they will take on the new CurrentTime with a zero elapsed time. This can in turn cause components to behave incorrectly + // if they are intending to trigger events at the precise StartTime (ie. DrawableStoryboardSample). + // + // By scheduling the start call, children are guaranteed to receive one frame at the original start time, allowing + // then to progress with a correct locally calculated elapsed time. SchedulerAfterChildren.Add(GameplayClock.Start); } } From fea31cc895b12c91252e611157093f94ad36a7a3 Mon Sep 17 00:00:00 2001 From: Jay L Date: Fri, 19 Aug 2022 22:57:28 +1000 Subject: [PATCH 242/532] introduce effective misscount, accuracy rescale --- .../Difficulty/TaikoPerformanceCalculator.cs | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 2c2dbddf13..9567277a61 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int countMeh; private int countMiss; + private double effectiveMissCount; + public TaikoPerformanceCalculator() : base(new TaikoRuleset()) { @@ -35,7 +37,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); - double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things + // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the misspenalty for shorter object counts lower than 1000, past 1000 is 1:1. + effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss; + + double multiplier = 1.13; if (score.Mods.Any(m => m is ModHidden)) multiplier *= 1.075; @@ -55,6 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { Difficulty = difficultyValue, Accuracy = accuracyValue, + EffectiveMissCount = effectiveMissCount, Total = totalValue }; } @@ -66,18 +72,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); difficultyValue *= lengthBonus; - difficultyValue *= Math.Pow(0.986, countMiss); + difficultyValue *= Math.Pow(0.986, effectiveMissCount); if (score.Mods.Any(m => m is ModEasy)) - difficultyValue *= 0.980; + difficultyValue *= 0.985; if (score.Mods.Any(m => m is ModHidden)) difficultyValue *= 1.025; + if (score.Mods.Any(m => m is ModHardRock)) + difficultyValue *= 1.050; + if (score.Mods.Any(m => m is ModFlashlight)) difficultyValue *= 1.05 * lengthBonus; - return difficultyValue * Math.Pow(score.Accuracy, 1.5); + return difficultyValue * Math.Pow(score.Accuracy, 2.0); } private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) @@ -85,18 +94,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (attributes.GreatHitWindow <= 0) return 0; - double accuracyValue = Math.Pow(140.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 12.0) * 27; + double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0; double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); accuracyValue *= lengthBonus; - // Slight HDFL Bonus for accuracy. + // Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values if (score.Mods.Any(m => m is ModFlashlight) && score.Mods.Any(m => m is ModHidden)) - accuracyValue *= 1.10 * lengthBonus; + accuracyValue *= Math.Max(1.050, 1.075 * lengthBonus); return accuracyValue; } private int totalHits => countGreat + countOk + countMeh + countMiss; + + private int totalSuccessfulHits => countGreat + countOk + countMeh; } } From b30fba143065e6eeefe5a6604df9525e1a4c66b7 Mon Sep 17 00:00:00 2001 From: Jay L Date: Fri, 19 Aug 2022 22:57:40 +1000 Subject: [PATCH 243/532] emc attribute --- .../Difficulty/TaikoPerformanceAttributes.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs index 68d0038b24..b61c13a2df 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs @@ -17,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty [JsonProperty("accuracy")] public double Accuracy { get; set; } + [JsonProperty("effective_miss_count")] + public double EffectiveMissCount { get; set; } + public override IEnumerable GetAttributesForDisplay() { foreach (var attribute in base.GetAttributesForDisplay()) From faf143b11aab5c7a1783fe624b84fecd2f0cf30e Mon Sep 17 00:00:00 2001 From: Jay L Date: Fri, 19 Aug 2022 23:15:38 +1000 Subject: [PATCH 244/532] fix comment --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 9567277a61..bc745da0fe 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); - // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the misspenalty for shorter object counts lower than 1000, past 1000 is 1:1. + // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss; double multiplier = 1.13; From c1da5091191bfac75eb2b5b3294f58addd1b64a5 Mon Sep 17 00:00:00 2001 From: Jay L Date: Fri, 19 Aug 2022 23:23:40 +1000 Subject: [PATCH 245/532] round numerical value this is painfully annoying me --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index bc745da0fe..7b0aa47ba5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty difficultyValue *= 1.050; if (score.Mods.Any(m => m is ModFlashlight)) - difficultyValue *= 1.05 * lengthBonus; + difficultyValue *= 1.050 * lengthBonus; return difficultyValue * Math.Pow(score.Accuracy, 2.0); } From d1519343f6040c2f0752956ad406f3c42152f731 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 19 Aug 2022 18:29:01 +0200 Subject: [PATCH 246/532] Improved visual tests for slider splitting --- .../Editor/TestSceneSliderSplitting.cs | 108 ++++++++++++++---- .../Sliders/SliderSelectionBlueprint.cs | 1 + 2 files changed, 88 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index 4693d56789..198d521a85 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -3,9 +3,9 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -27,15 +27,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private ComposeBlueprintContainer blueprintContainer => Editor.ChildrenOfType().First(); - private ContextMenuContainer contextMenuContainer - => Editor.ChildrenOfType().First(); - private Slider? slider; private PathControlPointVisualiser? visualiser; [Test] public void TestBasicSplit() { + double endTime = 0; + AddStep("add slider", () => { slider = new Slider @@ -52,6 +51,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }; EditorBeatmap.Add(slider); + + endTime = slider.EndTime; }); AddStep("select added slider", () => @@ -66,39 +67,104 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true; }); addContextMenuItemStep("Split control point"); + + AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 2 && + sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, slider.StartTime, + (new Vector2(0, 50), PathType.PerfectCurve), + (new Vector2(150, 200), null), + (new Vector2(300, 50), null) + ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], slider.StartTime, endTime, + (new Vector2(300, 50), PathType.PerfectCurve), + (new Vector2(400, 50), null), + (new Vector2(400, 200), null) + )); + + AddStep("undo", () => Editor.Undo()); + AddAssert("original slider restored", () => EditorBeatmap.HitObjects.Count == 1 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, endTime, + (new Vector2(0, 50), PathType.PerfectCurve), + (new Vector2(150, 200), null), + (new Vector2(300, 50), PathType.PerfectCurve), + (new Vector2(400, 50), null), + (new Vector2(400, 200), null) + )); } [Test] - public void TestStartTimeOffsetPlusDeselect() + public void TestDoubleSplit() { - HitCircle? circle = null; + double endTime = 0; - AddStep("add circle", () => + AddStep("add slider", () => { - circle = new HitCircle(); + slider = new Slider + { + Position = new Vector2(0, 50), + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(new Vector2(150, 150)), + new PathControlPoint(new Vector2(300, 0), PathType.Bezier), + new PathControlPoint(new Vector2(400, 0)), + new PathControlPoint(new Vector2(400, 150), PathType.Catmull), + new PathControlPoint(new Vector2(300, 200)), + new PathControlPoint(new Vector2(400, 250)) + }) + }; - EditorBeatmap.Add(circle); + EditorBeatmap.Add(slider); + + endTime = slider.EndTime; }); - AddStep("select added circle", () => + AddStep("select added slider", () => { - EditorBeatmap.SelectedHitObjects.Add(circle); + EditorBeatmap.SelectedHitObjects.Add(slider); + visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType().First(); }); - AddStep("add another circle", () => + moveMouseToControlPoint(2); + AddStep("select first control point", () => { - var circle2 = new HitCircle(); - - EditorBeatmap.Add(circle2); + if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true; }); - - AddStep("change time of selected circle and deselect", () => + moveMouseToControlPoint(4); + AddStep("select second control point", () => { - if (circle is null) return; - - circle.StartTime += 1; - EditorBeatmap.SelectedHitObjects.Clear(); + if (visualiser is not null) visualiser.Pieces[4].IsSelected.Value = true; }); + addContextMenuItemStep("Split 2 control points"); + + AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 3 && + sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime, + (new Vector2(0, 50), PathType.PerfectCurve), + (new Vector2(150, 200), null), + (new Vector2(300, 50), null) + ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], EditorBeatmap.HitObjects[0].GetEndTime(), slider.StartTime, + (new Vector2(300, 50), PathType.Bezier), + (new Vector2(400, 50), null), + (new Vector2(400, 200), null) + ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[2], EditorBeatmap.HitObjects[1].GetEndTime(), endTime, + (new Vector2(400, 200), PathType.Catmull), + (new Vector2(300, 250), null), + (new Vector2(400, 300), null) + )); + } + + private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints) + { + if (!Precision.AlmostEquals(s.StartTime, startTime, 1) || !Precision.AlmostEquals(s.EndTime, endTime, 1)) return false; + + int i = 0; + + foreach ((Vector2 pos, PathType? pathType) in expectedControlPoints) + { + var controlPoint = s.Path.ControlPoints[i++]; + + if (!Precision.AlmostEquals(controlPoint.Position + s.Position, pos) || controlPoint.Type != pathType) + return false; + } + + return true; } private void moveMouseToControlPoint(int index) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index e28dbb485d..d903ada6e2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -294,6 +294,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders Path = new SliderPath(splitControlPoints.Select(o => new PathControlPoint(o.Position - splitControlPoints[0].Position, o == splitControlPoints[^1] ? null : o.Type)).ToArray()) }; + // Increase the start time of the slider before adding the new slider so the new slider is immediately inserted at the correct index and internal state remains valid. HitObject.StartTime += 1; editorBeatmap.Add(newSlider); From 91e6f4c4eefeac62597e5ce18da8f6a8a0dceab7 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 19 Aug 2022 19:31:47 +0200 Subject: [PATCH 247/532] fix TestPerfectCurveChangeToBezier --- .../Editor/TestScenePathControlPointVisualiser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index f196353f87..6d93c3fcf9 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { AddStep($"click context menu item \"{contextMenuText}\"", () => { - MenuItem item = visualiser.ContextMenuItems[1].Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); + MenuItem item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); item?.Action?.Value(); }); From 65f7ecec835da17236201a96cd9182e791981529 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Aug 2022 00:26:04 +0200 Subject: [PATCH 248/532] moving all controlpoints at once for slider --- .../Edit/OsuSelectionHandler.cs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index f3c0a05bc2..bb78db68da 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -127,13 +127,14 @@ namespace osu.Game.Rulesets.Osu.Edit { didFlip = true; - foreach (var point in slider.Path.ControlPoints) - { - point.Position = new Vector2( - (direction == Direction.Horizontal ? -1 : 1) * point.Position.X, - (direction == Direction.Vertical ? -1 : 1) * point.Position.Y - ); - } + var controlPoints = slider.Path.ControlPoints.Select(p => + new PathControlPoint(new Vector2( + (direction == Direction.Horizontal ? -1 : 1) * p.Position.X, + (direction == Direction.Vertical ? -1 : 1) * p.Position.Y + ), p.Type)).ToArray(); + + slider.Path.ControlPoints.Clear(); + slider.Path.ControlPoints.AddRange(controlPoints); } } @@ -183,8 +184,11 @@ namespace osu.Game.Rulesets.Osu.Edit if (h is IHasPath path) { - foreach (var point in path.Path.ControlPoints) - point.Position = RotatePointAroundOrigin(point.Position, Vector2.Zero, delta); + var controlPoints = path.Path.ControlPoints.Select(p => + new PathControlPoint(RotatePointAroundOrigin(p.Position, Vector2.Zero, delta), p.Type)).ToArray(); + + path.Path.ControlPoints.Clear(); + path.Path.ControlPoints.AddRange(controlPoints); } } From 36e202c70ec33051b28d36be48b53d914d71ee05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 20 Aug 2022 11:38:43 +0900 Subject: [PATCH 249/532] Add inline comment explaining necessity to use `AddRange` for slider transform operations --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index bb78db68da..061c5008c5 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -133,6 +133,8 @@ namespace osu.Game.Rulesets.Osu.Edit (direction == Direction.Vertical ? -1 : 1) * p.Position.Y ), p.Type)).ToArray(); + // Importantly, update as a single operation so automatic adjustment of control points to different + // curve types does not unexpectedly trigger and change the slider's shape. slider.Path.ControlPoints.Clear(); slider.Path.ControlPoints.AddRange(controlPoints); } @@ -187,6 +189,8 @@ namespace osu.Game.Rulesets.Osu.Edit var controlPoints = path.Path.ControlPoints.Select(p => new PathControlPoint(RotatePointAroundOrigin(p.Position, Vector2.Zero, delta), p.Type)).ToArray(); + // Importantly, update as a single operation so automatic adjustment of control points to different + // curve types does not unexpectedly trigger and change the slider's shape. path.Path.ControlPoints.Clear(); path.Path.ControlPoints.AddRange(controlPoints); } From 885ea4270b5bbad78a48690abdaa5f95e136b5c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 20 Aug 2022 14:03:54 +0900 Subject: [PATCH 250/532] Reorder context menu items and tidy up surrounding code --- .../Components/PathControlPointVisualiser.cs | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 3fb7ec93e6..48e1d6405d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -105,12 +105,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components return true; } - // The slider can only be split on control points which connect two different slider segments. - private bool splittable(PathControlPointPiece p) => p.ControlPoint.Type.HasValue && p != Pieces[0] && p != Pieces[^1]; - private bool splitSelected() { - List toSplit = Pieces.Where(p => p.IsSelected.Value && splittable(p)).Select(p => p.ControlPoint).ToList(); + List toSplit = Pieces.Where(p => p.IsSelected.Value && isSplittable(p)).Select(p => p.ControlPoint).ToList(); // Ensure that there are any points to be split if (toSplit.Count == 0) @@ -127,6 +124,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components return true; } + private bool isSplittable(PathControlPointPiece p) => + // A slider can only be split on control points which connect two different slider segments. + p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault(); + private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -345,38 +346,42 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (count == 0) return null; - var splittablePieces = selectedPieces.Where(splittable).ToList(); + var splittablePieces = selectedPieces.Where(isSplittable).ToList(); int splittableCount = splittablePieces.Count; - List items = new List(); + List curveTypeItems = new List(); if (!selectedPieces.Contains(Pieces[0])) - items.Add(createMenuItemForPathType(null)); + curveTypeItems.Add(createMenuItemForPathType(null)); // todo: hide/disable items which aren't valid for selected points - items.Add(createMenuItemForPathType(PathType.Linear)); - items.Add(createMenuItemForPathType(PathType.PerfectCurve)); - items.Add(createMenuItemForPathType(PathType.Bezier)); - items.Add(createMenuItemForPathType(PathType.Catmull)); + curveTypeItems.Add(createMenuItemForPathType(PathType.Linear)); + curveTypeItems.Add(createMenuItemForPathType(PathType.PerfectCurve)); + curveTypeItems.Add(createMenuItemForPathType(PathType.Bezier)); + curveTypeItems.Add(createMenuItemForPathType(PathType.Catmull)); - var menuItems = new MenuItem[splittableCount > 0 ? 3 : 2]; - int i = 0; - - menuItems[i++] = new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, - () => DeleteSelected()); + var menuItems = new List + { + new OsuMenuItem("Curve type") + { + Items = curveTypeItems + } + }; if (splittableCount > 0) { - menuItems[i++] = new OsuMenuItem($"Split {"control point".ToQuantity(splittableCount, splittableCount > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", - MenuItemType.Destructive, () => splitSelected()); + menuItems.Add(new OsuMenuItem($"Split {"control point".ToQuantity(splittableCount, splittableCount > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", + MenuItemType.Destructive, + () => splitSelected())); } - menuItems[i] = new OsuMenuItem("Curve type") - { - Items = items - }; + menuItems.Add( + new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", + MenuItemType.Destructive, + () => DeleteSelected()) + ); - return menuItems; + return menuItems.ToArray(); } } From a1e849c4db045a111d93083d02931f97be5a9df3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 20 Aug 2022 16:22:35 +0900 Subject: [PATCH 251/532] Ensure that `DummyAPIAccess` runs all queued tasks on disposal --- osu.Game/Online/API/DummyAPIAccess.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 07d544260e..7dc34d1293 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -128,5 +128,13 @@ namespace osu.Game.Online.API IBindable IAPIProvider.Activity => Activity; public void FailNextLogin() => shouldFailNextLogin = true; + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + // Ensure (as much as we can) that any pending tasks are run. + Scheduler.Update(); + } } } From 8566e93c72f38b9459b4769e1eda18a7d95d4644 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 20 Aug 2022 17:19:17 +0900 Subject: [PATCH 252/532] Guard against `SubmittingPlayer` potentially getting stuck waiting on request forever --- osu.Game/Screens/Play/SubmittingPlayer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 02a95ae9eb..be77304076 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; @@ -84,7 +83,10 @@ namespace osu.Game.Screens.Play api.Queue(req); - tcs.Task.WaitSafely(); + // Generally a timeout would not happen here as APIAccess will timeout first. + if (!tcs.Task.Wait(60000)) + handleTokenFailure(new InvalidOperationException("Token retrieval timed out (request never run)")); + return true; void handleTokenFailure(Exception exception) From 614ae815c0f1a5ae6315507fd40c7aedf418c7e7 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Aug 2022 15:57:03 +0200 Subject: [PATCH 253/532] Added tests for making sure flipping and rotating retains perfect control point type --- .../Editor/TestSceneSliderSnapping.cs | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs index a72f2031c9..59f40894e0 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs @@ -12,6 +12,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Objects; @@ -55,9 +56,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { ControlPoints = { - new PathControlPoint(Vector2.Zero), - new PathControlPoint(OsuPlayfield.BASE_SIZE * 2 / 5), - new PathControlPoint(OsuPlayfield.BASE_SIZE * 3 / 5) + new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(new Vector2(136, 205)), + new PathControlPoint(new Vector2(-4, 226)) } } })); @@ -99,8 +100,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("move mouse to new point location", () => { var firstPiece = this.ChildrenOfType().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]); - var secondPiece = this.ChildrenOfType().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[1]); - InputManager.MoveMouseTo((firstPiece.ScreenSpaceDrawQuad.Centre + secondPiece.ScreenSpaceDrawQuad.Centre) / 2); + var pos = slider.Path.PositionAt(0.25d) + slider.Position; + InputManager.MoveMouseTo(firstPiece.Parent.ToScreenSpace(pos)); }); AddStep("move slider end", () => { @@ -175,6 +176,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertSliderSnapped(false); } + [Test] + public void TestRotatingSliderRetainsPerfectControlPointType() + { + OsuSelectionHandler selectionHandler = null; + + AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve); + + AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); + AddStep("rotate 90 degrees ccw", () => + { + selectionHandler = this.ChildrenOfType().Single(); + selectionHandler.HandleRotation(-90); + }); + + AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve); + } + [Test] public void TestFlippingSliderDoesNotSnap() { @@ -200,6 +218,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertSliderSnapped(false); } + [Test] + public void TestFlippingSliderRetainsPerfectControlPointType() + { + OsuSelectionHandler selectionHandler = null; + + AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve); + + AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); + AddStep("flip slider horizontally", () => + { + selectionHandler = this.ChildrenOfType().Single(); + selectionHandler.OnPressed(new KeyBindingPressEvent(InputManager.CurrentState, GlobalAction.EditorFlipVertically)); + }); + + AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve); + } + [Test] public void TestReversingSliderDoesNotSnap() { From 7732fb21d5de1873483c713e3a8a70354b616bdb Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 20 Aug 2022 16:09:02 +0200 Subject: [PATCH 254/532] fix code quality --- osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs index 59f40894e0..e864afe056 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs @@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestRotatingSliderRetainsPerfectControlPointType() { - OsuSelectionHandler selectionHandler = null; + OsuSelectionHandler selectionHandler; AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve); @@ -221,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestFlippingSliderRetainsPerfectControlPointType() { - OsuSelectionHandler selectionHandler = null; + OsuSelectionHandler selectionHandler; AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve); From 9386d352b869fad05071601fb834ed2e00ce19d3 Mon Sep 17 00:00:00 2001 From: naoei Date: Sat, 20 Aug 2022 21:48:35 -0400 Subject: [PATCH 255/532] Make StatisticItem.Name not nullable --- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- osu.Game/Screens/Ranking/Statistics/StatisticItem.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 555c272954..04bb08395b 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Taiko { Columns = new[] { - new StatisticItem(null, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index e1ea352f1c..e3ac054d1b 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// The name of this item. /// - public readonly LocalisableString? Name; + public readonly LocalisableString Name; /// /// A function returning the content to be displayed. @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// A function returning the content to be displayed. /// Whether this item requires hit events. If true, will not be called if no hit events are available. /// The of this item. This can be thought of as the column dimension of an encompassing . - public StatisticItem(LocalisableString? name, [NotNull] Func createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null) + public StatisticItem(LocalisableString name, [NotNull] Func createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null) { Name = name; RequiresHitEvents = requiresHitEvents; From 29ef1c8db8fcc5e53dbb9385768489149eaa5d6e Mon Sep 17 00:00:00 2001 From: naoei Date: Sat, 20 Aug 2022 21:48:53 -0400 Subject: [PATCH 256/532] Check if StatisticItem.Name is null or empty --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 2167e5e5ac..25749cb3d6 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -396,7 +396,7 @@ namespace osu.Game.Rulesets.Mania { Columns = new[] { - new StatisticItem(null, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new AverageHitError(score.HitEvents), new UnstableRate(score.HitEvents) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index f7df949414..7f58f29d4b 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -317,7 +317,7 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem(null, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] + new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] { new AverageHitError(timedHitEvents), new UnstableRate(timedHitEvents) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index 1cf46dcf04..1505585205 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; @@ -59,7 +60,7 @@ namespace osu.Game.Screens.Ranking.Statistics private static Drawable createHeader(StatisticItem item) { - if (item.Name == null) + if (LocalisableString.IsNullOrEmpty(item.Name)) return Empty(); return new FillFlowContainer @@ -82,7 +83,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = item.Name.Value, + Text = item.Name, Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), } } From aa15e84beab530d093119d4ca09f44fdc5f8eaeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 21 Aug 2022 23:07:42 +0200 Subject: [PATCH 257/532] Adjust rounding in mod select difficulty multiplier to match song select footer The 0.01 `Precision` spec on `DifficultyMultiplierDisplay.Current` would cause the difficulty multiplier to use a different midpoint rounding strategy than `double.ToString()`, which is the one that the song select footer relies on. For example, a value of 0.015 would be rounded down to 0.01 by `double.ToString()`, but rounded up to 0.02 by `BindableDouble`. Fix the discrepancy by just deleting the `Precision` spec. Since the value of the bindable would go through `ToLocalisableString(@"N2")` anyway, it was redundant as is. Fixes #19889. --- osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs index 255d01466f..835883fb93 100644 --- a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs +++ b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs @@ -31,10 +31,7 @@ namespace osu.Game.Overlays.Mods set => current.Current = value; } - private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(1) - { - Precision = 0.01 - }; + private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(1); private readonly Box underlayBackground; private readonly Box contentBackground; From c6a739f5a84acf2af368dbae2d80db911511e941 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Sun, 21 Aug 2022 23:09:33 -0400 Subject: [PATCH 258/532] Add date submitted sorting --- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 6 ++++++ osu.Game/Screens/Select/Filter/SortMode.cs | 3 +++ 2 files changed, 9 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 59d9318962..5a48d8d493 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -99,6 +99,12 @@ namespace osu.Game.Screens.Select.Carousel case SortMode.Difficulty: return compareUsingAggregateMax(otherSet, b => b.StarRating); + + case SortMode.DateSubmitted: + if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null) + return 0; + + return otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value); } } diff --git a/osu.Game/Screens/Select/Filter/SortMode.cs b/osu.Game/Screens/Select/Filter/SortMode.cs index 1e60ea3bac..c77bdbfbc6 100644 --- a/osu.Game/Screens/Select/Filter/SortMode.cs +++ b/osu.Game/Screens/Select/Filter/SortMode.cs @@ -20,6 +20,9 @@ namespace osu.Game.Screens.Select.Filter [LocalisableDescription(typeof(SortStrings), nameof(SortStrings.ArtistTracksBpm))] BPM, + [Description("Date Submitted")] + DateSubmitted, + [Description("Date Added")] DateAdded, From e1fa959f0b97189eeac72a20c95ed6fe667164c7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Aug 2022 12:59:52 +0900 Subject: [PATCH 259/532] Fix language change removing mod column bold text --- osu.Game/Overlays/Mods/ModSelectColumn.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index 0224631577..d5dc079628 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -159,12 +159,15 @@ namespace osu.Game.Overlays.Mods int wordIndex = 0; - headerText.AddText(text, t => + ITextPart part = headerText.AddText(text, t => { if (wordIndex == 0) t.Font = t.Font.With(weight: FontWeight.SemiBold); wordIndex += 1; }); + + // Reset the index so that if the parts are refreshed (e.g. through changes in localisation) the correct word is re-emboldened. + part.DrawablePartsRecreated += _ => wordIndex = 0; } [BackgroundDependencyLoader] From 85d0b7fc57ffe7c040167c49ee26fd0fa145a772 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 14:02:20 +0900 Subject: [PATCH 260/532] Reword class xmldoc to better explain that offset application is optional --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 90ff0588a6..62f1af3ef2 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -19,8 +19,8 @@ namespace osu.Game.Beatmaps /// A clock intended to be the single source-of-truth for beatmap timing. /// /// It provides some functionality: - /// - Applies (and tracks changes of) beatmap, user, and platform offsets. - /// - Adjusts operations to account for said offsets, seeking in raw time values. + /// - Optionally applies (and tracks changes of) beatmap, user, and platform offsets (see ctor argument applyOffsets). + /// - Adjusts operations to account for any applied offsets, seeking in raw "beatmap" time values. /// - Exposes track length. /// - Allows changing the source to a new track (for cases like editor track updating). /// From ba23ce75c2d16bbf40d7d945d426644d044645f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 14:02:41 +0900 Subject: [PATCH 261/532] Make `FramedBeatmapClock.Track` non-null --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 62f1af3ef2..b4a96ed46a 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -31,12 +31,12 @@ namespace osu.Game.Beatmaps /// /// The length of the underlying beatmap track. Will default to 60 seconds if unavailable. /// - public double TrackLength => Track?.Length ?? 60000; + public double TrackLength => Track.Length; /// /// The underlying beatmap track, if available. /// - public Track? Track { get; private set; } // TODO: virtual rather than null? + public Track Track { get; private set; } = new TrackVirtual(60000); /// /// The total frequency adjustment from pause transforms. Should eventually be handled in a better way. @@ -144,7 +144,7 @@ namespace osu.Game.Beatmaps public void ChangeSource(IClock? source) { - Track = source as Track; + Track = source as Track ?? new TrackVirtual(60000); decoupledClock.ChangeSource(source); } From 17a1df281c34c5c5ffa8b25f34acdaa3b0b9a4ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 14:03:51 +0900 Subject: [PATCH 262/532] Fix incorrect implicit null specification for user audio offset bindable --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index b4a96ed46a..3166f688ea 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -49,7 +49,7 @@ namespace osu.Game.Beatmaps private readonly IFrameBasedClock finalClockSource; - private Bindable userAudioOffset = null!; + private Bindable? userAudioOffset; private IDisposable? beatmapOffsetSubscription; From af2e82d7d54fb19cc04dd14715348ea5ebf56b61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 14:11:06 +0900 Subject: [PATCH 263/532] Move operation of setting `GameplayClockContainer.StartTime` to `Reset` call --- .../Gameplay/TestSceneStoryboardSamples.cs | 3 ++- .../Screens/Edit/GameplayTest/EditorPlayer.cs | 6 +++++- .../Spectate/MultiSpectatorScreen.cs | 3 +-- .../Screens/Play/GameplayClockContainer.cs | 21 +++++++------------ .../Play/MasterGameplayClockContainer.cs | 2 +- osu.Game/Screens/Play/Player.cs | 5 ++--- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index afdcedb485..dcaeadf82a 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -107,12 +107,13 @@ namespace osu.Game.Tests.Gameplay Add(gameplayContainer = new MasterGameplayClockContainer(working, start_time) { - StartTime = start_time, Child = new FrameStabilityContainer { Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) } }); + + gameplayContainer.Reset(start_time); }); AddStep("start time", () => gameplayContainer.Start()); diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index fd230a97bc..94975b6b5e 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -27,7 +27,11 @@ namespace osu.Game.Screens.Edit.GameplayTest } protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) - => new MasterGameplayClockContainer(beatmap, gameplayStart) { StartTime = editorState.Time }; + { + var masterGameplayClockContainer = new MasterGameplayClockContainer(beatmap, gameplayStart); + masterGameplayClockContainer.Reset(editorState.Time); + return masterGameplayClockContainer; + } protected override void LoadComplete() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index f200702e80..940f9078a8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -192,8 +192,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate .DefaultIfEmpty(0) .Min(); - masterClockContainer.StartTime = startTime; - masterClockContainer.Reset(true); + masterClockContainer.Reset(startTime, true); // Although the clock has been started, this flag is set to allow for later synchronisation state changes to also be able to start it. canStartMasterClock = true; diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 6dab13c950..f3dcf1a64c 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -38,23 +38,13 @@ namespace osu.Game.Screens.Play /// /// The time from which the clock should start. Will be seeked to on calling . - /// Settting a start time will to the new value. + /// Can be adjusted by calling with a time value. /// /// /// By default, a value of zero will be used. /// Importantly, the value will be inferred from the current beatmap in by default. /// - public double StartTime - { - get => startTime; - set - { - startTime = value; - Reset(); - } - } - - private double startTime; + public double StartTime { get; private set; } public virtual IEnumerable NonGameplayAdjustments => Enumerable.Empty(); @@ -139,13 +129,18 @@ namespace osu.Game.Screens.Play /// /// Resets this and the source to an initial state ready for gameplay. /// + /// The time to seek to on resetting. If null, the existing will be used. /// Whether to start the clock immediately, if not already started. - public void Reset(bool startClock = false) + public void Reset(double? time = null, bool startClock = false) { // Manually stop the source in order to not affect the IsPaused state. GameplayClock.Stop(); ensureSourceClockSet(); + + if (time != null) + StartTime = time.Value; + Seek(StartTime); if (!IsPaused.Value || startClock) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index eca0c92f8f..c3c92eb0fe 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Play this.beatmap = beatmap; this.skipTargetTime = skipTargetTime; - StartTime = findEarliestStartTime(); + Reset(findEarliestStartTime()); } private double findEarliestStartTime() diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 6827ff04d3..d8db41c833 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -640,8 +640,7 @@ namespace osu.Game.Screens.Play bool wasFrameStable = DrawableRuleset.FrameStablePlayback; DrawableRuleset.FrameStablePlayback = false; - GameplayClockContainer.StartTime = time; - GameplayClockContainer.Reset(); + GameplayClockContainer.Reset(time); // Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek. frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable); @@ -1012,7 +1011,7 @@ namespace osu.Game.Screens.Play if (GameplayClockContainer.IsRunning) throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"); - GameplayClockContainer.Reset(true); + GameplayClockContainer.Reset(startClock: true); } public override void OnSuspending(ScreenTransitionEvent e) From e6b669db8e62c5d25c1db41d4285b2ab8b3a2ced Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 14:14:44 +0900 Subject: [PATCH 264/532] Elaborate with example of `GameplayClockContainer` managing its own `Stop` state --- osu.Game/Screens/Play/GameplayClockContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index f3dcf1a64c..a00ae27781 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -88,6 +88,7 @@ namespace osu.Game.Screens.Play isPaused.Value = false; // the clock may be stopped via internal means (ie. not via `IsPaused`). + // see Reset() calling `GameplayClock.Stop()` as one example. if (!GameplayClock.IsRunning) { // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time From 9d31f61ca91cd8b3a1815692070083079febe93a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 14:35:44 +0900 Subject: [PATCH 265/532] Don't throw when a ruleset type is completely missing --- osu.Game/Rulesets/RealmRulesetStore.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/RealmRulesetStore.cs b/osu.Game/Rulesets/RealmRulesetStore.cs index 7b1f3a3f6c..dba7f47f2f 100644 --- a/osu.Game/Rulesets/RealmRulesetStore.cs +++ b/osu.Game/Rulesets/RealmRulesetStore.cs @@ -68,8 +68,14 @@ namespace osu.Game.Rulesets { try { - var resolvedType = Type.GetType(r.InstantiationInfo) - ?? throw new RulesetLoadException(@"Type could not be resolved"); + var resolvedType = Type.GetType(r.InstantiationInfo); + + if (resolvedType == null) + { + // ruleset DLL was probably deleted. + r.Available = false; + continue; + } var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo ?? throw new RulesetLoadException(@"Instantiation failure"); From d199b3b1009afcf776d1f14bba86a910fff37f3f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 14:51:00 +0900 Subject: [PATCH 266/532] Update `GetVariantName` to also support localisation --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- .../Settings/Sections/Input/VariantBindingsSubsection.cs | 2 -- osu.Game/Rulesets/Ruleset.cs | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 25749cb3d6..ac6060ceed 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -312,7 +312,7 @@ namespace osu.Game.Rulesets.Mania return Array.Empty(); } - public override string GetVariantName(int variant) + public override LocalisableString GetVariantName(int variant) { switch (getPlayfieldType(variant)) { diff --git a/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs index 23f91fba4b..a0f069b3bb 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Localisation; using osu.Game.Rulesets; diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 50ce6b3b12..0968d78ed7 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -289,7 +289,7 @@ namespace osu.Game.Rulesets /// /// The variant. /// A descriptive name of the variant. - public virtual string GetVariantName(int variant) => string.Empty; + public virtual LocalisableString GetVariantName(int variant) => string.Empty; /// /// For rulesets which support legacy (osu-stable) replay conversion, this method will create an empty replay frame From 19bba143ee74fd21eed399ca35b1f806bc1d43b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 15:57:24 +0900 Subject: [PATCH 267/532] Fix editor crashing on mobile releases --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index e41802808f..8f3e077050 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.UI this.gameplayStartTime = gameplayStartTime; } - [BackgroundDependencyLoader] + [BackgroundDependencyLoader(true)] private void load(IGameplayClock? gameplayClock) { if (gameplayClock != null) From 09ef13908ca8c9f82cd691bc47c6c1657e14e133 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Mon, 22 Aug 2022 03:20:27 -0400 Subject: [PATCH 268/532] Adjust to reviews --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 1 + osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 5b17b412ae..f1f7c47e1b 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -56,6 +56,7 @@ namespace osu.Game.Screens.Select.Carousel criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode); match &= criteria.Sort != SortMode.DateRanked || BeatmapInfo.BeatmapSet?.DateRanked != null; + match &= criteria.Sort != SortMode.DateSubmitted || BeatmapInfo.BeatmapSet?.DateSubmitted != null; match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 5a48d8d493..8298c73fe9 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -101,6 +101,7 @@ namespace osu.Game.Screens.Select.Carousel return compareUsingAggregateMax(otherSet, b => b.StarRating); case SortMode.DateSubmitted: + // Beatmaps which have no submitted date should already be filtered away in this mode. if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null) return 0; From f5710d8000d3bc11c4a2065f411105c53e249914 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 16:10:55 +0900 Subject: [PATCH 269/532] Add ruleset API versioning --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 ++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 ++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 ++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 ++ osu.Game/Rulesets/Ruleset.cs | 17 +++++++++++++++++ 5 files changed, 25 insertions(+) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index ed151855b1..f94bf276a0 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Catch public const string SHORT_NAME = "fruits"; + public override string RulesetAPIVersionSupported => "internal"; + public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { new KeyBinding(InputKey.Z, CatchAction.MoveLeft), diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index ac6060ceed..b1fe4b30c4 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -60,6 +60,8 @@ namespace osu.Game.Rulesets.Mania public const string SHORT_NAME = "mania"; + public override string RulesetAPIVersionSupported => "internal"; + public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this); public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new ManiaLegacySkinTransformer(skin, beatmap); diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 7f58f29d4b..4400dfbb65 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -54,6 +54,8 @@ namespace osu.Game.Rulesets.Osu public const string SHORT_NAME = "osu"; + public override string RulesetAPIVersionSupported => "internal"; + public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { new KeyBinding(InputKey.Z, OsuAction.LeftButton), diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 04bb08395b..275c7144a7 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -49,6 +49,8 @@ namespace osu.Game.Rulesets.Taiko public const string SHORT_NAME = "taiko"; + public override string RulesetAPIVersionSupported => "internal"; + public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre), diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 0968d78ed7..63f5906f46 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -44,6 +44,23 @@ namespace osu.Game.Rulesets private static readonly ConcurrentDictionary mod_reference_cache = new ConcurrentDictionary(); + /// + /// Version history: + /// 2022.205.0 FramedReplayInputHandler.CollectPendingInputs renamed to FramedReplayHandler.CollectReplayInputs. + /// 2022.822.0 All strings return values have been converted to LocalisableString to allow for localisation support. + /// + public const string CURRENT_RULESET_API_VERSION = "2022.822.0"; + + /// + /// Define the ruleset API version supported by this ruleset. + /// Ruleset implementations should be updated to support the latest version to ensure they can still be loaded. + /// + /// + /// When updating a ruleset to support the latest API, you should set this to . + /// See https://github.com/ppy/osu/wiki/Breaking-Changes for full details on required ongoing changes. + /// + public virtual string RulesetAPIVersionSupported => string.Empty; + /// /// A queryable source containing all available mods. /// Call for consumption purposes. From 758a554180fb61f21dbc8b9eae6d90d674358ded Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 16:31:25 +0900 Subject: [PATCH 270/532] Add basic check for correct ruleset API version --- osu.Game/Rulesets/RealmRulesetStore.cs | 28 +++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/RealmRulesetStore.cs b/osu.Game/Rulesets/RealmRulesetStore.cs index dba7f47f2f..590f118b68 100644 --- a/osu.Game/Rulesets/RealmRulesetStore.cs +++ b/osu.Game/Rulesets/RealmRulesetStore.cs @@ -77,9 +77,16 @@ namespace osu.Game.Rulesets continue; } - var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo + var instance = (Activator.CreateInstance(resolvedType) as Ruleset); + var instanceInfo = instance?.RulesetInfo ?? throw new RulesetLoadException(@"Instantiation failure"); + if (!checkRulesetUpToDate(instance)) + { + throw new ArgumentOutOfRangeException(nameof(instance.RulesetAPIVersionSupported), + $"Ruleset API version is too old (was {instance.RulesetAPIVersionSupported}, expected {Ruleset.CURRENT_RULESET_API_VERSION})"); + } + // If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution. // To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw. resolvedType.Assembly.GetTypes(); @@ -104,6 +111,25 @@ namespace osu.Game.Rulesets }); } + private bool checkRulesetUpToDate(Ruleset instance) + { + switch (instance.RulesetAPIVersionSupported) + { + // The default `virtual` implementation leaves the version string empty. + // Consider rulesets which haven't override the version as up-to-date for now. + // At some point (once ruleset devs add versioning), we'll probably want to disallow this for deployed builds. + case @"": + // Rulesets which are bundled with the game. Saves having to update their versions each bump. + case @"internal": + // Ruleset is up-to-date, all good. + case Ruleset.CURRENT_RULESET_API_VERSION: + return true; + + default: + return false; + } + } + private void testRulesetCompatibility(RulesetInfo rulesetInfo) { // do various operations to ensure that we are in a good state. From c2036d3893a0c556742e88a42fd6363fe0ecb766 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Mon, 22 Aug 2022 03:39:46 -0400 Subject: [PATCH 271/532] Moved filter exclusion --- .../Screens/Select/Carousel/CarouselBeatmap.cs | 3 --- .../Screens/Select/Carousel/CarouselBeatmapSet.cs | 14 +++++++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index f1f7c47e1b..03490ff37b 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -55,9 +55,6 @@ namespace osu.Game.Screens.Select.Carousel match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) || criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode); - match &= criteria.Sort != SortMode.DateRanked || BeatmapInfo.BeatmapSet?.DateRanked != null; - match &= criteria.Sort != SortMode.DateSubmitted || BeatmapInfo.BeatmapSet?.DateSubmitted != null; - match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating); if (match && criteria.SearchTerms.Length > 0) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 8298c73fe9..1c82bdedcf 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -129,7 +129,19 @@ namespace osu.Game.Screens.Select.Carousel public override void Filter(FilterCriteria criteria) { base.Filter(criteria); - Filtered.Value = Items.All(i => i.Filtered.Value); + bool match = Items.All(i => i.Filtered.Value); + + if (BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) + { + // only check ruleset equality or convertability for selected beatmap + Filtered.Value = !match; + return; + } + + match &= criteria.Sort != SortMode.DateRanked || BeatmapSet?.DateRanked != null; + match &= criteria.Sort != SortMode.DateSubmitted || BeatmapSet?.DateSubmitted != null; + + Filtered.Value = match; } public override string ToString() => BeatmapSet.ToString(); From c86a75aa5fb125b79da55b7efcf77fbdb831175f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 17:03:20 +0900 Subject: [PATCH 272/532] Update `OsuConfigManager` in line with `ConfigManager` logging changes --- osu.Game/Configuration/OsuConfigManager.cs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index fb585e9cbd..5f49557685 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -4,10 +4,8 @@ #nullable disable using System; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.Linq; using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions; @@ -31,6 +29,12 @@ namespace osu.Game.Configuration [ExcludeFromDynamicCompile] public class OsuConfigManager : IniConfigManager { + public OsuConfigManager(Storage storage) + : base(storage) + { + Migrate(); + } + protected override void InitialiseDefaults() { // UI/selection defaults @@ -172,12 +176,9 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.LastProcessedMetadataId, -1); } - public IDictionary GetLoggableState() => - new Dictionary(ConfigStore.Where(kvp => !keyContainsPrivateInformation(kvp.Key)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToString())); - - private static bool keyContainsPrivateInformation(OsuSetting argKey) + protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup) { - switch (argKey) + switch (lookup) { case OsuSetting.Token: return true; @@ -186,12 +187,6 @@ namespace osu.Game.Configuration return false; } - public OsuConfigManager(Storage storage) - : base(storage) - { - Migrate(); - } - public void Migrate() { // arrives as 2020.123.0 From 22072ee16a9a5fd9bfe9acd7f8567f79f9f98088 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 17:03:30 +0900 Subject: [PATCH 273/532] Include framework configuration in sentry output --- osu.Game/Utils/SentryLogger.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index 33dc548e9a..8c39a2d15a 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -9,6 +9,7 @@ using System.Net; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Configuration; using osu.Framework.Logging; using osu.Framework.Statistics; using osu.Game.Beatmaps; @@ -112,8 +113,8 @@ namespace osu.Game.Utils scope.Contexts[@"config"] = new { - Game = game.Dependencies.Get().GetLoggableState() - // TODO: add framework config here. needs some consideration on how to expose. + Game = game.Dependencies.Get().GetCurrentConfigurationForLogging(), + Framework = game.Dependencies.Get().GetCurrentConfigurationForLogging(), }; game.Dependencies.Get().Run(realm => From 3acbcac4d1a215bb0e17ebb36c3f93309d018cb0 Mon Sep 17 00:00:00 2001 From: Jay L Date: Mon, 22 Aug 2022 19:45:51 +1000 Subject: [PATCH 274/532] fix NaN PP on 0 object count --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 7b0aa47ba5..6b1ea58129 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. - effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss; + effectiveMissCount = Math.Max(1.0, Math.Min(0, 1000.0 / totalSuccessfulHits)) * countMiss; double multiplier = 1.13; From 5d3d8681d498aaaa5990d7955f30b570e2fc3215 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Aug 2022 19:14:06 +0900 Subject: [PATCH 275/532] Invert creation of clocks in multi spectator --- .../OnlinePlay/TestSceneCatchUpSyncManager.cs | 85 +++++++------------ .../Spectate/CatchUpSyncManager.cs | 14 +-- .../Multiplayer/Spectate/ISyncManager.cs | 12 +-- .../Spectate/MultiSpectatorScreen.cs | 7 +- .../Multiplayer/Spectate/PlayerArea.cs | 8 +- 5 files changed, 50 insertions(+), 76 deletions(-) diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index 6c639ee539..88c4850fa7 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -4,11 +4,13 @@ #nullable disable using System; +using System.Collections.Generic; using NUnit.Framework; -using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; +using osu.Game.Screens.Play; using osu.Game.Tests.Visual; namespace osu.Game.Tests.OnlinePlay @@ -16,20 +18,34 @@ namespace osu.Game.Tests.OnlinePlay [HeadlessTest] public class TestSceneCatchUpSyncManager : OsuTestScene { - private TestManualClock master; + private GameplayClockContainer master; private CatchUpSyncManager syncManager; - private TestSpectatorPlayerClock player1; - private TestSpectatorPlayerClock player2; + private Dictionary clocksById; + private ISpectatorPlayerClock player1; + private ISpectatorPlayerClock player2; [SetUp] public void Setup() { - syncManager = new CatchUpSyncManager(master = new TestManualClock()); - syncManager.AddPlayerClock(player1 = new TestSpectatorPlayerClock(1)); - syncManager.AddPlayerClock(player2 = new TestSpectatorPlayerClock(2)); + syncManager = new CatchUpSyncManager(master = new GameplayClockContainer(new TestManualClock())); + player1 = syncManager.AddClock(); + player2 = syncManager.AddClock(); - Schedule(() => Child = syncManager); + clocksById = new Dictionary + { + { player1, 1 }, + { player2, 2 } + }; + + Schedule(() => + { + Children = new Drawable[] + { + syncManager, + master + }; + }); } [Test] @@ -129,8 +145,8 @@ namespace osu.Game.Tests.OnlinePlay assertPlayerClockState(() => player1, false); } - private void setWaiting(Func playerClock, bool waiting) - => AddStep($"set player clock {playerClock().Id} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = waiting); + private void setWaiting(Func playerClock, bool waiting) + => AddStep($"set player clock {clocksById[playerClock()]} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = waiting); private void setAllWaiting(bool waiting) => AddStep($"set all player clocks waiting = {waiting}", () => { @@ -144,51 +160,14 @@ namespace osu.Game.Tests.OnlinePlay /// /// clock.Time = master.Time - offsetFromMaster /// - private void setPlayerClockTime(Func playerClock, double offsetFromMaster) - => AddStep($"set player clock {playerClock().Id} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster)); + private void setPlayerClockTime(Func playerClock, double offsetFromMaster) + => AddStep($"set player clock {clocksById[playerClock()]} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster)); - private void assertCatchingUp(Func playerClock, bool catchingUp) => - AddAssert($"player clock {playerClock().Id} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp); + private void assertCatchingUp(Func playerClock, bool catchingUp) => + AddAssert($"player clock {clocksById[playerClock()]} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp); - private void assertPlayerClockState(Func playerClock, bool running) - => AddAssert($"player clock {playerClock().Id} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running); - - private class TestSpectatorPlayerClock : TestManualClock, ISpectatorPlayerClock - { - public Bindable WaitingOnFrames { get; } = new Bindable(true); - - public bool IsCatchingUp { get; set; } - - public IFrameBasedClock Source - { - set => throw new NotImplementedException(); - } - - public readonly int Id; - - public TestSpectatorPlayerClock(int id) - { - Id = id; - - WaitingOnFrames.BindValueChanged(waiting => - { - if (waiting.NewValue) - Stop(); - else - Start(); - }); - } - - public void ProcessFrame() - { - } - - public double ElapsedFrameTime => 0; - - public double FramesPerSecond => 0; - - public FrameTimeInfo TimeInfo => default; - } + private void assertPlayerClockState(Func playerClock, bool running) + => AddAssert($"player clock {clocksById[playerClock()]} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running); private class TestManualClock : ManualClock, IAdjustableClock { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs index 663025923c..b0ebe23292 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs @@ -5,11 +5,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Timing; +using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { @@ -38,7 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// The master clock which is used to control the timing of all player clocks clocks. /// - public IAdjustableClock MasterClock { get; } + public GameplayClockContainer MasterClock { get; } public IBindable MasterState => masterState; @@ -52,18 +51,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private bool hasStarted; private double? firstStartAttemptTime; - public CatchUpSyncManager(IAdjustableClock master) + public CatchUpSyncManager(GameplayClockContainer master) { MasterClock = master; } - public void AddPlayerClock(ISpectatorPlayerClock clock) + public ISpectatorPlayerClock AddClock() { - Debug.Assert(!playerClocks.Contains(clock)); + var clock = new CatchUpSpectatorPlayerClock { Source = MasterClock }; playerClocks.Add(clock); + return clock; } - public void RemovePlayerClock(ISpectatorPlayerClock clock) + public void RemoveClock(ISpectatorPlayerClock clock) { playerClocks.Remove(clock); clock.Stop(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs index 577100d4ba..49707ed975 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs @@ -5,7 +5,7 @@ using System; using osu.Framework.Bindables; -using osu.Framework.Timing; +using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { @@ -22,7 +22,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// The master clock which player clocks should synchronise to. /// - IAdjustableClock MasterClock { get; } + GameplayClockContainer MasterClock { get; } /// /// An event which is invoked when the state of is changed. @@ -30,15 +30,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate IBindable MasterState { get; } /// - /// Adds an to manage. + /// Adds a new managed . /// - /// The to add. - void AddPlayerClock(ISpectatorPlayerClock clock); + /// The added . + ISpectatorPlayerClock AddClock(); /// /// Removes an , stopping it from being managed by this . /// /// The to remove. - void RemovePlayerClock(ISpectatorPlayerClock clock); + void RemoveClock(ISpectatorPlayerClock clock); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 7ed0be50e5..96f08ef446 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -125,10 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }; for (int i = 0; i < Users.Count; i++) - { - grid.Add(instances[i] = new PlayerArea(Users[i], masterClockContainer)); - syncManager.AddPlayerClock(instances[i].GameplayClock); - } + grid.Add(instances[i] = new PlayerArea(Users[i], syncManager.AddClock())); LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users) { @@ -242,7 +239,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate var instance = instances.Single(i => i.UserId == userId); instance.FadeColour(colours.Gray4, 400, Easing.OutQuint); - syncManager.RemovePlayerClock(instance.GameplayClock); + syncManager.RemoveClock(instance.GameplayClock); } public override bool OnBackButton() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 302d04b531..a013a9e41d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -11,7 +11,6 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; @@ -45,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// The used to control the gameplay running state of a loaded . /// [NotNull] - public readonly ISpectatorPlayerClock GameplayClock = new CatchUpSpectatorPlayerClock(); + public readonly ISpectatorPlayerClock GameplayClock; /// /// The currently-loaded score. @@ -58,9 +57,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly LoadingLayer loadingLayer; private OsuScreenStack stack; - public PlayerArea(int userId, IFrameBasedClock masterClock) + public PlayerArea(int userId, [NotNull] ISpectatorPlayerClock clock) { UserId = userId; + GameplayClock = clock; RelativeSizeAxes = Axes.Both; Masking = true; @@ -77,8 +77,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }; audioContainer.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); - - GameplayClock.Source = masterClock; } [Resolved] From 489e172a7660c07030eb38799f9e23681cecf813 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Aug 2022 19:43:18 +0900 Subject: [PATCH 276/532] Simplify track start/stop/paused tracking --- .../Screens/Play/GameplayClockContainer.cs | 79 ++++++++++--------- .../Play/MasterGameplayClockContainer.cs | 39 +++++---- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index a00ae27781..897d2cbdcd 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -74,39 +74,41 @@ namespace osu.Game.Screens.Play GameplayClock = new FramedBeatmapClock(sourceClock, applyOffsets) { IsCoupled = false }, Content }; - - IsPaused.BindValueChanged(OnIsPausedChanged); } /// /// Starts gameplay and marks un-paused state. /// - public virtual void Start() + public void Start() { - ensureSourceClockSet(); + if (!isPaused.Value) + return; isPaused.Value = false; - // the clock may be stopped via internal means (ie. not via `IsPaused`). - // see Reset() calling `GameplayClock.Stop()` as one example. - if (!GameplayClock.IsRunning) - { - // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time - // This accounts for the clock source potentially taking time to enter a completely stopped state - Seek(GameplayClock.CurrentTime); + ensureSourceClockSet(); - // The case which cause this to be added is FrameStabilityContainer, which manages its own current and elapsed time. - // Because we generally update our own current time quicker than children can query it (via Start/Seek/Update), - // this means that the first frame ever exposed to children may have a non-zero current time. - // - // If the child component is not aware of the parent ElapsedFrameTime (which is the case for FrameStabilityContainer) - // they will take on the new CurrentTime with a zero elapsed time. This can in turn cause components to behave incorrectly - // if they are intending to trigger events at the precise StartTime (ie. DrawableStoryboardSample). - // - // By scheduling the start call, children are guaranteed to receive one frame at the original start time, allowing - // then to progress with a correct locally calculated elapsed time. - SchedulerAfterChildren.Add(GameplayClock.Start); - } + // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time + // This accounts for the clock source potentially taking time to enter a completely stopped state + Seek(GameplayClock.CurrentTime); + + // The case which cause this to be added is FrameStabilityContainer, which manages its own current and elapsed time. + // Because we generally update our own current time quicker than children can query it (via Start/Seek/Update), + // this means that the first frame ever exposed to children may have a non-zero current time. + // + // If the child component is not aware of the parent ElapsedFrameTime (which is the case for FrameStabilityContainer) + // they will take on the new CurrentTime with a zero elapsed time. This can in turn cause components to behave incorrectly + // if they are intending to trigger events at the precise StartTime (ie. DrawableStoryboardSample). + // + // By scheduling the start call, children are guaranteed to receive one frame at the original start time, allowing + // then to progress with a correct locally calculated elapsed time. + SchedulerAfterChildren.Add(() => + { + if (isPaused.Value) + return; + + StartGameplayClock(); + }); } /// @@ -125,7 +127,17 @@ namespace osu.Game.Screens.Play /// /// Stops gameplay and marks paused state. /// - public void Stop() => isPaused.Value = true; + public void Stop() + { + if (isPaused.Value) + return; + + isPaused.Value = true; + StopGameplayClock(); + } + + protected virtual void StartGameplayClock() => GameplayClock.Start(); + protected virtual void StopGameplayClock() => GameplayClock.Stop(); /// /// Resets this and the source to an initial state ready for gameplay. @@ -134,8 +146,9 @@ namespace osu.Game.Screens.Play /// Whether to start the clock immediately, if not already started. public void Reset(double? time = null, bool startClock = false) { - // Manually stop the source in order to not affect the IsPaused state. - GameplayClock.Stop(); + bool wasPaused = isPaused.Value; + + Stop(); ensureSourceClockSet(); @@ -144,7 +157,7 @@ namespace osu.Game.Screens.Play Seek(StartTime); - if (!IsPaused.Value || startClock) + if (!wasPaused || startClock) Start(); } @@ -167,18 +180,6 @@ namespace osu.Game.Screens.Play ChangeSource(SourceClock); } - /// - /// Invoked when the value of is changed to start or stop the clock. - /// - /// Whether the clock should now be paused. - protected virtual void OnIsPausedChanged(ValueChangedEvent isPaused) - { - if (isPaused.NewValue) - GameplayClock.Stop(); - else - GameplayClock.Start(); - } - #region IAdjustableClock bool IAdjustableClock.Seek(double position) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index c3c92eb0fe..168a5658f5 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -84,29 +84,23 @@ namespace osu.Game.Screens.Play return time; } - protected override void OnIsPausedChanged(ValueChangedEvent isPaused) + protected override void StopGameplayClock() { if (IsLoaded) { // During normal operation, the source is stopped after performing a frequency ramp. - if (isPaused.NewValue) + this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 200, Easing.Out).OnComplete(_ => { - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 200, Easing.Out).OnComplete(_ => - { - if (IsPaused.Value == isPaused.NewValue) - base.OnIsPausedChanged(isPaused); - }); - } - else - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 200, Easing.In); + if (IsPaused.Value) + base.StopGameplayClock(); + }); } else { - if (isPaused.NewValue) - base.OnIsPausedChanged(isPaused); + base.StopGameplayClock(); // If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations. - GameplayClock.ExternalPauseFrequencyAdjust.Value = isPaused.NewValue ? 0 : 1; + GameplayClock.ExternalPauseFrequencyAdjust.Value = 0; // We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment. // Without doing this, an initial seek may be performed with the wrong offset. @@ -114,10 +108,25 @@ namespace osu.Game.Screens.Play } } - public override void Start() + protected override void StartGameplayClock() { addSourceClockAdjustments(); - base.Start(); + + base.StartGameplayClock(); + + if (IsLoaded) + { + this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 200, Easing.In); + } + else + { + // If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations. + GameplayClock.ExternalPauseFrequencyAdjust.Value = 1; + + // We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment. + // Without doing this, an initial seek may be performed with the wrong offset. + GameplayClock.ProcessFrame(); + } } /// From c59298f0ce6f310300306bda1f7d3655c29d8c76 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Aug 2022 21:52:43 +0900 Subject: [PATCH 277/532] Enable NRT --- .../Spectate/CatchUpSpectatorPlayerClock.cs | 13 +++++------ .../Spectate/CatchUpSyncManager.cs | 6 ++--- .../Spectate/ISpectatorPlayerClock.cs | 2 -- .../Multiplayer/Spectate/ISyncManager.cs | 4 +--- .../Spectate/MultiSpectatorScreen.cs | 22 +++++++++---------- .../Multiplayer/Spectate/PlayerArea.cs | 21 +++++++----------- 6 files changed, 27 insertions(+), 41 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs index 34388bf9b1..93e1ea3c24 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs @@ -17,15 +17,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public const double CATCHUP_RATE = 2; - /// - /// The source clock. - /// - public IFrameBasedClock? Source { get; set; } + public IFrameBasedClock Source { get; set; } public double CurrentTime { get; private set; } public bool IsRunning { get; private set; } + public CatchUpSpectatorPlayerClock(IFrameBasedClock source) + { + Source = source; + } + public void Reset() => CurrentTime = 0; public void Start() => IsRunning = true; @@ -67,9 +69,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate ElapsedFrameTime = 0; FramesPerSecond = 0; - if (Source == null) - return; - Source.ProcessFrame(); if (IsRunning) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs index b0ebe23292..ede2600671 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -32,7 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public const double MAXIMUM_START_DELAY = 15000; - public event Action ReadyToStart; + public event Action? ReadyToStart; /// /// The master clock which is used to control the timing of all player clocks clocks. @@ -58,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public ISpectatorPlayerClock AddClock() { - var clock = new CatchUpSpectatorPlayerClock { Source = MasterClock }; + var clock = new CatchUpSpectatorPlayerClock(MasterClock); playerClocks.Add(clock); return clock; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs index 194a3bdcc2..b2ecb105c2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Timing; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs index 49707ed975..9281e4c1cf 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Bindables; using osu.Game.Screens.Play; @@ -17,7 +15,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// An event which is invoked when gameplay is ready to start. /// - event Action ReadyToStart; + event Action? ReadyToStart; /// /// The master clock which player clocks should synchronise to. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 96f08ef446..5a24d74955 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -1,13 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -42,17 +40,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override UserActivity InitialActivity => new UserActivity.SpectatingMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [Resolved] - private MultiplayerClient multiplayerClient { get; set; } + private MultiplayerClient multiplayerClient { get; set; } = null!; private readonly PlayerArea[] instances; - private MasterGameplayClockContainer masterClockContainer; - private ISyncManager syncManager; - private PlayerGrid grid; - private MultiSpectatorLeaderboard leaderboard; - private PlayerArea currentAudioSource; + private MasterGameplayClockContainer masterClockContainer = null!; + private ISyncManager syncManager = null!; + private PlayerGrid grid = null!; + private MultiSpectatorLeaderboard leaderboard = null!; + private PlayerArea? currentAudioSource; private bool canStartMasterClock; private readonly Room room; @@ -178,7 +176,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } } - private bool isCandidateAudioSource([CanBeNull] ISpectatorPlayerClock clock) + private bool isCandidateAudioSource(ISpectatorPlayerClock? clock) => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value; private void onReadyToStart() @@ -186,7 +184,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate // Seek the master clock to the gameplay time. // This is chosen as the first available frame in the players' replays, which matches the seek by each individual SpectatorPlayer. double startTime = instances.Where(i => i.Score != null) - .SelectMany(i => i.Score.Replay.Frames) + .SelectMany(i => i.Score.AsNonNull().Replay.Frames) .Select(f => f.Time) .DefaultIfEmpty(0) .Min(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index a013a9e41d..7e679383c4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -1,11 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -28,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// Raised after is called on . /// - public event Action OnGameplayStarted; + public event Action? OnGameplayStarted; /// /// Whether a is loaded in the area. @@ -43,21 +40,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// The used to control the gameplay running state of a loaded . /// - [NotNull] public readonly ISpectatorPlayerClock GameplayClock; /// /// The currently-loaded score. /// - [CanBeNull] - public Score Score { get; private set; } + public Score? Score { get; private set; } + + [Resolved] + private IBindable beatmap { get; set; } = null!; private readonly BindableDouble volumeAdjustment = new BindableDouble(); private readonly Container gameplayContent; private readonly LoadingLayer loadingLayer; - private OsuScreenStack stack; + private OsuScreenStack? stack; - public PlayerArea(int userId, [NotNull] ISpectatorPlayerClock clock) + public PlayerArea(int userId, ISpectatorPlayerClock clock) { UserId = userId; GameplayClock = clock; @@ -79,10 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate audioContainer.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); } - [Resolved] - private IBindable beatmap { get; set; } - - public void LoadScore([NotNull] Score score) + public void LoadScore(Score score) { if (Score != null) throw new InvalidOperationException($"Cannot load a new score on a {nameof(PlayerArea)} that has an existing score."); From 55f1b43329612a62184cafdf500b1e4371214e64 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Mon, 22 Aug 2022 13:41:36 -0400 Subject: [PATCH 278/532] Removed check --- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 1c82bdedcf..6c134a4ab8 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -131,13 +131,6 @@ namespace osu.Game.Screens.Select.Carousel base.Filter(criteria); bool match = Items.All(i => i.Filtered.Value); - if (BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) - { - // only check ruleset equality or convertability for selected beatmap - Filtered.Value = !match; - return; - } - match &= criteria.Sort != SortMode.DateRanked || BeatmapSet?.DateRanked != null; match &= criteria.Sort != SortMode.DateSubmitted || BeatmapSet?.DateSubmitted != null; From e8d4bc4497c3fe3f7f2606db471a48fce87393cb Mon Sep 17 00:00:00 2001 From: Khang Date: Mon, 22 Aug 2022 21:04:26 -0400 Subject: [PATCH 279/532] Allow NaN during beatmap parsing if desired --- osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs | 7 ++++++- osu.Game/Beatmaps/Formats/Parsing.cs | 8 ++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs b/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs index f3673b0e7f..339063633a 100644 --- a/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs @@ -14,7 +14,12 @@ namespace osu.Game.Tests.Beatmaps.Formats public class ParsingTest { [Test] - public void TestNaNHandling() => allThrow("NaN"); + public void TestNaNHandling() + { + allThrow("NaN"); + Assert.That(Parsing.ParseFloat("NaN", allowNaN: true), Is.NaN); + Assert.That(Parsing.ParseDouble("NaN", allowNaN: true), Is.NaN); + } [Test] public void TestBadStringHandling() => allThrow("Random string 123"); diff --git a/osu.Game/Beatmaps/Formats/Parsing.cs b/osu.Game/Beatmaps/Formats/Parsing.cs index ce04f27020..9b0d200077 100644 --- a/osu.Game/Beatmaps/Formats/Parsing.cs +++ b/osu.Game/Beatmaps/Formats/Parsing.cs @@ -17,26 +17,26 @@ namespace osu.Game.Beatmaps.Formats public const double MAX_PARSE_VALUE = int.MaxValue; - public static float ParseFloat(string input, float parseLimit = (float)MAX_PARSE_VALUE) + public static float ParseFloat(string input, float parseLimit = (float)MAX_PARSE_VALUE, bool allowNaN = false) { float output = float.Parse(input, CultureInfo.InvariantCulture); if (output < -parseLimit) throw new OverflowException("Value is too low"); if (output > parseLimit) throw new OverflowException("Value is too high"); - if (float.IsNaN(output)) throw new FormatException("Not a number"); + if (!allowNaN && float.IsNaN(output)) throw new FormatException("Not a number"); return output; } - public static double ParseDouble(string input, double parseLimit = MAX_PARSE_VALUE) + public static double ParseDouble(string input, double parseLimit = MAX_PARSE_VALUE, bool allowNaN = false) { double output = double.Parse(input, CultureInfo.InvariantCulture); if (output < -parseLimit) throw new OverflowException("Value is too low"); if (output > parseLimit) throw new OverflowException("Value is too high"); - if (double.IsNaN(output)) throw new FormatException("Not a number"); + if (!allowNaN && double.IsNaN(output)) throw new FormatException("Not a number"); return output; } From 9f08c474caf57d8a84a510653c72a7f01ccf2b8d Mon Sep 17 00:00:00 2001 From: Khang Date: Mon, 22 Aug 2022 21:44:25 -0400 Subject: [PATCH 280/532] Treat NaN slider velocity timing points as 1.0x but without slider ticks --- .../Objects/JuiceStream.cs | 5 ++++- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 2 +- .../Mods/OsuModStrictTracking.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 ++++++- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 2 +- .../Beatmaps/SliderEventGenerationTest.cs | 17 ++++++++++----- .../ControlPoints/DifficultyControlPoint.cs | 15 ++++++++++--- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 8 ++++++- .../Rulesets/Objects/SliderEventGenerator.cs | 21 +++++++++++-------- 9 files changed, 57 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 311e15116e..a9c7d938d2 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -40,6 +40,9 @@ namespace osu.Game.Rulesets.Catch.Objects [JsonIgnore] public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity; + [JsonIgnore] + public bool GenerateTicks => DifficultyControlPoint.GenerateTicks; + /// /// The length of one span of this . /// @@ -64,7 +67,7 @@ namespace osu.Game.Rulesets.Catch.Objects int nodeIndex = 0; SliderEventDescriptor? lastEvent = null; - foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken)) + foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, GenerateTicks, cancellationToken)) { // generate tiny droplets since the last point if (lastEvent != null) diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 22fab15c1b..4ec039c405 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Mania.Objects private void createTicks(CancellationToken cancellationToken) { - if (tickSpacing == 0) + if (tickSpacing == 0 || !DifficultyControlPoint.GenerateTicks) return; for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 67b19124e1..eb59122686 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Mods protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken); + var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, GenerateTicks, cancellationToken); foreach (var e in sliderEvents) { diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index a5468ff613..31117c0836 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -142,6 +142,11 @@ namespace osu.Game.Rulesets.Osu.Objects /// public double TickDistanceMultiplier = 1; + /// + /// Whether this should generate s. + /// + public bool GenerateTicks { get; private set; } + /// /// Whether this 's judgement is fully handled by its nested s. /// If false, this will be judged proportionally to the number of nested s hit. @@ -170,13 +175,14 @@ namespace osu.Game.Rulesets.Osu.Objects Velocity = scoringDistance / timingPoint.BeatLength; TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; + GenerateTicks = DifficultyControlPoint.GenerateTicks; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { base.CreateNestedHitObjects(cancellationToken); - var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken); + var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, GenerateTicks, cancellationToken); foreach (var e in sliderEvents) { diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index e1619e2857..09e465c37b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Taiko.Objects private void createTicks(CancellationToken cancellationToken) { - if (tickSpacing == 0) + if (tickSpacing == 0 || !DifficultyControlPoint.GenerateTicks) return; bool first = true; diff --git a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs index d30ab3dea1..077398f015 100644 --- a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs +++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestSingleSpan() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null, true).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestRepeat() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null, true).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestNonEvenTicks() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null, true).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestLegacyLastTickOffset() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100, true).ToArray(); Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick)); Assert.That(events[2].Time, Is.EqualTo(900)); @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Beatmaps const double velocity = 5; const double min_distance = velocity * 10; - var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0, true).ToArray(); Assert.Multiple(() => { @@ -114,5 +114,12 @@ namespace osu.Game.Tests.Beatmaps } }); } + + [Test] + public void TestNoTickGeneration() + { + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null, false).ToArray(); + Assert.That(events.Any(e => e.Type == SliderEventType.Tick), Is.False); + } } } diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index c199d1da59..dfa3552469 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -40,13 +40,21 @@ namespace osu.Game.Beatmaps.ControlPoints set => SliderVelocityBindable.Value = value; } + /// + /// Whether or not slider ticks should be generated at this control point. + /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). + /// + public bool GenerateTicks { get; set; } = true; + public override bool IsRedundant(ControlPoint? existing) => existing is DifficultyControlPoint existingDifficulty - && SliderVelocity == existingDifficulty.SliderVelocity; + && SliderVelocity == existingDifficulty.SliderVelocity + && GenerateTicks == existingDifficulty.GenerateTicks; public override void CopyFrom(ControlPoint other) { SliderVelocity = ((DifficultyControlPoint)other).SliderVelocity; + GenerateTicks = ((DifficultyControlPoint)other).GenerateTicks; base.CopyFrom(other); } @@ -57,8 +65,9 @@ namespace osu.Game.Beatmaps.ControlPoints public bool Equals(DifficultyControlPoint? other) => base.Equals(other) - && SliderVelocity == other.SliderVelocity; + && SliderVelocity == other.SliderVelocity + && GenerateTicks == other.GenerateTicks; - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity); + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity, GenerateTicks); } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 89d3465ab6..3d3661745a 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -373,7 +373,9 @@ namespace osu.Game.Beatmaps.Formats string[] split = line.Split(','); double time = getOffsetTime(Parsing.ParseDouble(split[0].Trim())); - double beatLength = Parsing.ParseDouble(split[1].Trim()); + double beatLength = Parsing.ParseDouble(split[1].Trim(), allowNaN: true); + + // If beatLength is NaN, speedMultiplier should still be 1 because all comparisons against NaN are false. double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; TimeSignature timeSignature = TimeSignature.SimpleQuadruple; @@ -412,6 +414,9 @@ namespace osu.Game.Beatmaps.Formats if (timingChange) { + if (double.IsNaN(beatLength)) + throw new InvalidDataException("Beat length cannot be NaN in a timing control point"); + var controlPoint = CreateTimingControlPoint(); controlPoint.BeatLength = beatLength; @@ -425,6 +430,7 @@ namespace osu.Game.Beatmaps.Formats #pragma warning restore 618 { SliderVelocity = speedMultiplier, + GenerateTicks = !double.IsNaN(beatLength), }, timingChange); var effectPoint = new EffectControlPoint diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index d32a7cb16d..b77d15d565 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Objects { // ReSharper disable once MethodOverloadWithOptionalParameter public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, - double? legacyLastTickOffset, CancellationToken cancellationToken = default) + double? legacyLastTickOffset, bool shouldGenerateTicks, CancellationToken cancellationToken = default) { // A very lenient maximum length of a slider for ticks to be generated. // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage. @@ -41,16 +41,19 @@ namespace osu.Game.Rulesets.Objects double spanStartTime = startTime + span * spanDuration; bool reversed = span % 2 == 1; - var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken); - - if (reversed) + if (shouldGenerateTicks) { - // For repeat spans, ticks are returned in reverse-StartTime order, which is undesirable for some rulesets - ticks = ticks.Reverse(); - } + var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken); - foreach (var e in ticks) - yield return e; + if (reversed) + { + // For repeat spans, ticks are returned in reverse-StartTime order, which is undesirable for some rulesets + ticks = ticks.Reverse(); + } + + foreach (var e in ticks) + yield return e; + } if (span < spanCount - 1) { From 8f708c1dcf30e7300b8f0c71ab48989a234f5cda Mon Sep 17 00:00:00 2001 From: Khang Date: Mon, 22 Aug 2022 22:43:44 -0400 Subject: [PATCH 281/532] Turn GenerateTicks into a bindable to pass code quality tests --- .../ControlPoints/DifficultyControlPoint.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index dfa3552469..ca49d316e0 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -16,6 +16,7 @@ namespace osu.Game.Beatmaps.ControlPoints public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint { SliderVelocityBindable = { Disabled = true }, + GenerateTicksBindable = { Disabled = true }, }; /// @@ -29,6 +30,12 @@ namespace osu.Game.Beatmaps.ControlPoints MaxValue = 10 }; + /// + /// Whether or not slider ticks should be generated at this control point. + /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). + /// + public readonly BindableBool GenerateTicksBindable = new BindableBool(true); + public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1; /// @@ -44,7 +51,11 @@ namespace osu.Game.Beatmaps.ControlPoints /// Whether or not slider ticks should be generated at this control point. /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). /// - public bool GenerateTicks { get; set; } = true; + public bool GenerateTicks + { + get => GenerateTicksBindable.Value; + set => GenerateTicksBindable.Value = value; + } public override bool IsRedundant(ControlPoint? existing) => existing is DifficultyControlPoint existingDifficulty From a81672f3dc9e885651f78852675fadb76940ace2 Mon Sep 17 00:00:00 2001 From: Khang Date: Mon, 22 Aug 2022 23:31:24 -0400 Subject: [PATCH 282/532] Use an infinite tick distance instead of directly disabling tick generation for SliderEventGenerator --- .../Objects/JuiceStream.cs | 5 +---- .../Mods/OsuModStrictTracking.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 10 ++------- .../Beatmaps/SliderEventGenerationTest.cs | 17 +++++---------- .../Rulesets/Objects/SliderEventGenerator.cs | 21 ++++++++----------- 5 files changed, 18 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index a9c7d938d2..311e15116e 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -40,9 +40,6 @@ namespace osu.Game.Rulesets.Catch.Objects [JsonIgnore] public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity; - [JsonIgnore] - public bool GenerateTicks => DifficultyControlPoint.GenerateTicks; - /// /// The length of one span of this . /// @@ -67,7 +64,7 @@ namespace osu.Game.Rulesets.Catch.Objects int nodeIndex = 0; SliderEventDescriptor? lastEvent = null; - foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, GenerateTicks, cancellationToken)) + foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken)) { // generate tiny droplets since the last point if (lastEvent != null) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index eb59122686..67b19124e1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Mods protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, GenerateTicks, cancellationToken); + var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken); foreach (var e in sliderEvents) { diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 31117c0836..0689c49085 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -142,11 +142,6 @@ namespace osu.Game.Rulesets.Osu.Objects /// public double TickDistanceMultiplier = 1; - /// - /// Whether this should generate s. - /// - public bool GenerateTicks { get; private set; } - /// /// Whether this 's judgement is fully handled by its nested s. /// If false, this will be judged proportionally to the number of nested s hit. @@ -174,15 +169,14 @@ namespace osu.Game.Rulesets.Osu.Objects double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; - TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; - GenerateTicks = DifficultyControlPoint.GenerateTicks; + TickDistance = DifficultyControlPoint.GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { base.CreateNestedHitObjects(cancellationToken); - var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, GenerateTicks, cancellationToken); + var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken); foreach (var e in sliderEvents) { diff --git a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs index 077398f015..d30ab3dea1 100644 --- a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs +++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestSingleSpan() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null, true).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestRepeat() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null, true).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestNonEvenTicks() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null, true).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestLegacyLastTickOffset() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100, true).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray(); Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick)); Assert.That(events[2].Time, Is.EqualTo(900)); @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Beatmaps const double velocity = 5; const double min_distance = velocity * 10; - var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0, true).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray(); Assert.Multiple(() => { @@ -114,12 +114,5 @@ namespace osu.Game.Tests.Beatmaps } }); } - - [Test] - public void TestNoTickGeneration() - { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null, false).ToArray(); - Assert.That(events.Any(e => e.Type == SliderEventType.Tick), Is.False); - } } } diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index b77d15d565..d32a7cb16d 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Objects { // ReSharper disable once MethodOverloadWithOptionalParameter public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, - double? legacyLastTickOffset, bool shouldGenerateTicks, CancellationToken cancellationToken = default) + double? legacyLastTickOffset, CancellationToken cancellationToken = default) { // A very lenient maximum length of a slider for ticks to be generated. // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage. @@ -41,20 +41,17 @@ namespace osu.Game.Rulesets.Objects double spanStartTime = startTime + span * spanDuration; bool reversed = span % 2 == 1; - if (shouldGenerateTicks) + var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken); + + if (reversed) { - var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken); - - if (reversed) - { - // For repeat spans, ticks are returned in reverse-StartTime order, which is undesirable for some rulesets - ticks = ticks.Reverse(); - } - - foreach (var e in ticks) - yield return e; + // For repeat spans, ticks are returned in reverse-StartTime order, which is undesirable for some rulesets + ticks = ticks.Reverse(); } + foreach (var e in ticks) + yield return e; + if (span < spanCount - 1) { yield return new SliderEventDescriptor From 1191b6c080491c454286ddcab0178a204a7a518f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Aug 2022 13:44:41 +0900 Subject: [PATCH 283/532] Remove unused `Source_Set` implementation on `ISpectatorPlayerClock` --- .../Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs | 2 +- .../OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs index 93e1ea3c24..8d8f6a373a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public const double CATCHUP_RATE = 2; - public IFrameBasedClock Source { get; set; } + public readonly IFrameBasedClock Source; public double CurrentTime { get; private set; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs index b2ecb105c2..a2e6df9282 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs @@ -33,10 +33,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// Of note, this will be false if this clock is *ahead* of the master clock. /// bool IsCatchingUp { get; set; } - - /// - /// The source clock - /// - IFrameBasedClock Source { set; } } } From 553897f2f024483b0c2282086f9db8de81604844 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Aug 2022 13:52:43 +0900 Subject: [PATCH 284/532] Remove `AddClock` method to `CreateManagedClock` --- osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs | 4 ++-- .../OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs | 4 ++-- .../OnlinePlay/Multiplayer/Spectate/ISyncManager.cs | 8 ++++---- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index 88c4850fa7..db14dc95b2 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -29,8 +29,8 @@ namespace osu.Game.Tests.OnlinePlay public void Setup() { syncManager = new CatchUpSyncManager(master = new GameplayClockContainer(new TestManualClock())); - player1 = syncManager.AddClock(); - player2 = syncManager.AddClock(); + player1 = syncManager.CreateManagedClock(); + player2 = syncManager.CreateManagedClock(); clocksById = new Dictionary { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs index ede2600671..4e563ec69a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs @@ -54,14 +54,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate MasterClock = master; } - public ISpectatorPlayerClock AddClock() + public ISpectatorPlayerClock CreateManagedClock() { var clock = new CatchUpSpectatorPlayerClock(MasterClock); playerClocks.Add(clock); return clock; } - public void RemoveClock(ISpectatorPlayerClock clock) + public void RemoveManagedClock(ISpectatorPlayerClock clock) { playerClocks.Remove(clock); clock.Stop(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs index 9281e4c1cf..5615e02336 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs @@ -28,15 +28,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate IBindable MasterState { get; } /// - /// Adds a new managed . + /// Create a new managed . /// - /// The added . - ISpectatorPlayerClock AddClock(); + /// The newly created . + ISpectatorPlayerClock CreateManagedClock(); /// /// Removes an , stopping it from being managed by this . /// /// The to remove. - void RemoveClock(ISpectatorPlayerClock clock); + void RemoveManagedClock(ISpectatorPlayerClock clock); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 5a24d74955..3d04ae8f3c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -123,7 +123,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }; for (int i = 0; i < Users.Count; i++) - grid.Add(instances[i] = new PlayerArea(Users[i], syncManager.AddClock())); + grid.Add(instances[i] = new PlayerArea(Users[i], syncManager.CreateManagedClock())); LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users) { @@ -237,7 +237,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate var instance = instances.Single(i => i.UserId == userId); instance.FadeColour(colours.Gray4, 400, Easing.OutQuint); - syncManager.RemoveClock(instance.GameplayClock); + syncManager.RemoveManagedClock(instance.GameplayClock); } public override bool OnBackButton() From fbe8de2757ca2a752163bd6a2d61a4d788b55301 Mon Sep 17 00:00:00 2001 From: Khang Date: Tue, 23 Aug 2022 00:57:25 -0400 Subject: [PATCH 285/532] Disable the GetHashCode warning instead of using bindables --- .../ControlPoints/DifficultyControlPoint.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index ca49d316e0..ddb2b1e95c 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -16,7 +16,6 @@ namespace osu.Game.Beatmaps.ControlPoints public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint { SliderVelocityBindable = { Disabled = true }, - GenerateTicksBindable = { Disabled = true }, }; /// @@ -30,12 +29,6 @@ namespace osu.Game.Beatmaps.ControlPoints MaxValue = 10 }; - /// - /// Whether or not slider ticks should be generated at this control point. - /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). - /// - public readonly BindableBool GenerateTicksBindable = new BindableBool(true); - public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1; /// @@ -51,11 +44,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// Whether or not slider ticks should be generated at this control point. /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). /// - public bool GenerateTicks - { - get => GenerateTicksBindable.Value; - set => GenerateTicksBindable.Value = value; - } + public bool GenerateTicks { get; set; } = true; public override bool IsRedundant(ControlPoint? existing) => existing is DifficultyControlPoint existingDifficulty @@ -79,6 +68,7 @@ namespace osu.Game.Beatmaps.ControlPoints && SliderVelocity == other.SliderVelocity && GenerateTicks == other.GenerateTicks; + // ReSharper disable once NonReadonlyMemberInGetHashCode public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity, GenerateTicks); } } From 690e048864351f03b75902383ed1c785ed0d5eba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Aug 2022 17:00:00 +0900 Subject: [PATCH 286/532] Ensure all initial imports are completed before running playlist overlay tests steps --- .../Visual/UserInterface/TestScenePlaylistOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index e67eebf721..fde2d82b16 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -66,6 +66,9 @@ namespace osu.Game.Tests.Visual.UserInterface } beatmapSets.First().ToLive(Realm); + + // Ensure all the initial imports are present before running any tests. + Realm.Run(r => r.Refresh()); }); [Test] From a62deae3cce7a7f2fdd02f80ee5d88bc86272680 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Aug 2022 17:06:32 +0900 Subject: [PATCH 287/532] Use local realm rather than fetching from dependencies --- osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index fde2d82b16..f280230da7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Add collection", () => { - Dependencies.Get().Write(r => + Realm.Write(r => { r.RemoveAll(); r.Add(new BeatmapCollection("wang")); From 9a579871c088774196835ee37ed89ce36ea181c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Aug 2022 17:20:26 +0900 Subject: [PATCH 288/532] Remove pointless initial import --- .../Visual/UserInterface/TestScenePlaylistOverlay.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index f280230da7..d87bcfa5dd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; @@ -39,8 +38,6 @@ namespace osu.Game.Tests.Visual.UserInterface Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); Dependencies.Cache(Realm); - - beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); } [SetUp] From db004c9d9f441360b6bcffb0743090964ba47d85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Aug 2022 17:33:09 +0900 Subject: [PATCH 289/532] Fix collection dropdown potentially overwriting value change with schedule hotfix --- osu.Game/Collections/CollectionDropdown.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Collections/CollectionDropdown.cs b/osu.Game/Collections/CollectionDropdown.cs index 43a4d90aa8..71341f51a4 100644 --- a/osu.Game/Collections/CollectionDropdown.cs +++ b/osu.Game/Collections/CollectionDropdown.cs @@ -75,7 +75,14 @@ namespace osu.Game.Collections // changes. It's not great but honestly the whole dropdown menu structure isn't great. This needs to be fixed, but I'll issue // a warning that it's going to be a frustrating journey. Current.Value = allBeatmaps; - Schedule(() => Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection.ID == selectedItem?.ID) ?? filters[0]); + Schedule(() => + { + // current may have changed before the scheduled call is run. + if (Current.Value != allBeatmaps) + return; + + Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection.ID == selectedItem?.ID) ?? filters[0]; + }); // Trigger a re-filter if the current item was in the change set. if (selectedItem != null && changes != null) From 29fed0c4a3c1cfb445d0a57d78771ac4fcada63d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Aug 2022 18:32:56 +0900 Subject: [PATCH 290/532] Avoid setting the source clock until gameplay is ready to start Without this change, the audio track may audibly seek during load proceedings. --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 3 +-- osu.Game/Screens/Play/GameplayClockContainer.cs | 4 ++-- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 3166f688ea..c86f25640f 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -70,14 +70,13 @@ namespace osu.Game.Beatmaps set => decoupledClock.IsCoupled = value; } - public FramedBeatmapClock(IClock? sourceClock = null, bool applyOffsets = false) + public FramedBeatmapClock(bool applyOffsets = false) { this.applyOffsets = applyOffsets; // A decoupled clock is used to ensure precise time values even when the host audio subsystem is not reporting // high precision times (on windows there's generally only 5-10ms reporting intervals, as an example). decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; - decoupledClock.ChangeSource(sourceClock); if (applyOffsets) { diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 897d2cbdcd..21fffb2992 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Play /// By default, a value of zero will be used. /// Importantly, the value will be inferred from the current beatmap in by default. /// - public double StartTime { get; private set; } + public double StartTime { get; protected set; } public virtual IEnumerable NonGameplayAdjustments => Enumerable.Empty(); @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Play InternalChildren = new Drawable[] { - GameplayClock = new FramedBeatmapClock(sourceClock, applyOffsets) { IsCoupled = false }, + GameplayClock = new FramedBeatmapClock(applyOffsets) { IsCoupled = false }, Content }; } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 168a5658f5..238817ad05 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Play this.beatmap = beatmap; this.skipTargetTime = skipTargetTime; - Reset(findEarliestStartTime()); + StartTime = findEarliestStartTime(); } private double findEarliestStartTime() From c840977acb72cd9d10fe90caf7c1d6ccacfa9c44 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Aug 2022 20:42:20 +0900 Subject: [PATCH 291/532] Fix filtering potentially not running after new items added --- osu.Game/Overlays/Music/Playlist.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Music/Playlist.cs b/osu.Game/Overlays/Music/Playlist.cs index 15fc54a337..b48257a61a 100644 --- a/osu.Game/Overlays/Music/Playlist.cs +++ b/osu.Game/Overlays/Music/Playlist.cs @@ -27,6 +27,12 @@ namespace osu.Game.Overlays.Music set => base.Padding = value; } + protected override void OnItemsChanged() + { + base.OnItemsChanged(); + Filter(currentCriteria); + } + public void Filter(FilterCriteria criteria) { var items = (SearchContainer>>)ListContainer; @@ -44,12 +50,12 @@ namespace osu.Game.Overlays.Music public Live? FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); - protected override OsuRearrangeableListItem> CreateOsuDrawable(Live item) => new PlaylistItem(item) - { - InSelectedCollection = currentCriteria.Collection?.PerformRead(c => item.Value.Beatmaps.Select(b => b.MD5Hash).Any(c.BeatmapMD5Hashes.Contains)) != false, - SelectedSet = { BindTarget = SelectedSet }, - RequestSelection = set => RequestSelection?.Invoke(set) - }; + protected override OsuRearrangeableListItem> CreateOsuDrawable(Live item) => + new PlaylistItem(item) + { + SelectedSet = { BindTarget = SelectedSet }, + RequestSelection = set => RequestSelection?.Invoke(set) + }; protected override FillFlowContainer>> CreateListFillFlowContainer() => new SearchContainer>> { From c1ced85b5e4fd5afe86e064774fda164be0ce88c Mon Sep 17 00:00:00 2001 From: Khang Date: Tue, 23 Aug 2022 14:07:18 -0400 Subject: [PATCH 292/532] Move GenerateTicks to LegacyDifficultyControlPoint and remove support for NaN slider velocity support for other rulesets (at least for now) --- .../Objects/JuiceStream.cs | 6 +++++- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 10 +++++++++- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 2 +- .../ControlPoints/DifficultyControlPoint.cs | 16 +++------------ .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 1 - osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 20 ++++++++++++++++--- 7 files changed, 36 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 311e15116e..2a5564297c 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -11,6 +11,7 @@ using Newtonsoft.Json; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -50,9 +51,12 @@ namespace osu.Game.Rulesets.Catch.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); +#pragma warning disable 618 + bool generateTicks = (DifficultyControlPoint as LegacyBeatmapDecoder.LegacyDifficultyControlPoint)?.GenerateTicks ?? true; +#pragma warning restore 618 velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength; - tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate; + tickDistanceFactor = generateTicks ? (base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate) : double.PositiveInfinity; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 4ec039c405..22fab15c1b 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Mania.Objects private void createTicks(CancellationToken cancellationToken) { - if (tickSpacing == 0 || !DifficultyControlPoint.GenerateTicks) + if (tickSpacing == 0) return; for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 0689c49085..df1252c060 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -14,6 +14,8 @@ using osu.Framework.Caching; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Formats; +using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; @@ -166,10 +168,16 @@ namespace osu.Game.Rulesets.Osu.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); + var legacyInfo = controlPointInfo as LegacyControlPointInfo; +#pragma warning disable 618 + var legacyDifficultyPoint = legacyInfo?.DifficultyPointAt(StartTime) as LegacyBeatmapDecoder.LegacyDifficultyControlPoint; +#pragma warning restore 618 + double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; + bool generateTicks = legacyDifficultyPoint?.GenerateTicks ?? true; Velocity = scoringDistance / timingPoint.BeatLength; - TickDistance = DifficultyControlPoint.GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity; + TickDistance = generateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 09e465c37b..e1619e2857 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Taiko.Objects private void createTicks(CancellationToken cancellationToken) { - if (tickSpacing == 0 || !DifficultyControlPoint.GenerateTicks) + if (tickSpacing == 0) return; bool first = true; diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index ddb2b1e95c..c199d1da59 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -40,21 +40,13 @@ namespace osu.Game.Beatmaps.ControlPoints set => SliderVelocityBindable.Value = value; } - /// - /// Whether or not slider ticks should be generated at this control point. - /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). - /// - public bool GenerateTicks { get; set; } = true; - public override bool IsRedundant(ControlPoint? existing) => existing is DifficultyControlPoint existingDifficulty - && SliderVelocity == existingDifficulty.SliderVelocity - && GenerateTicks == existingDifficulty.GenerateTicks; + && SliderVelocity == existingDifficulty.SliderVelocity; public override void CopyFrom(ControlPoint other) { SliderVelocity = ((DifficultyControlPoint)other).SliderVelocity; - GenerateTicks = ((DifficultyControlPoint)other).GenerateTicks; base.CopyFrom(other); } @@ -65,10 +57,8 @@ namespace osu.Game.Beatmaps.ControlPoints public bool Equals(DifficultyControlPoint? other) => base.Equals(other) - && SliderVelocity == other.SliderVelocity - && GenerateTicks == other.GenerateTicks; + && SliderVelocity == other.SliderVelocity; - // ReSharper disable once NonReadonlyMemberInGetHashCode - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity, GenerateTicks); + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity); } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 3d3661745a..f064d89e19 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -430,7 +430,6 @@ namespace osu.Game.Beatmaps.Formats #pragma warning restore 618 { SliderVelocity = speedMultiplier, - GenerateTicks = !double.IsNaN(beatLength), }, timingChange); var effectPoint = new EffectControlPoint diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 52e760a068..c3fd16e86f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -168,11 +168,18 @@ namespace osu.Game.Beatmaps.Formats /// public double BpmMultiplier { get; private set; } + /// + /// Whether or not slider ticks should be generated at this control point. + /// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991). + /// + public bool GenerateTicks { get; private set; } = true; + public LegacyDifficultyControlPoint(double beatLength) : this() { // Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?). BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1; + GenerateTicks = !double.IsNaN(beatLength); } public LegacyDifficultyControlPoint() @@ -180,11 +187,17 @@ namespace osu.Game.Beatmaps.Formats SliderVelocityBindable.Precision = double.Epsilon; } + public override bool IsRedundant(ControlPoint? existing) + => existing is LegacyDifficultyControlPoint existingLegacyDifficulty + && base.IsRedundant(existing) + && GenerateTicks == existingLegacyDifficulty.GenerateTicks; + public override void CopyFrom(ControlPoint other) { base.CopyFrom(other); BpmMultiplier = ((LegacyDifficultyControlPoint)other).BpmMultiplier; + GenerateTicks = ((LegacyDifficultyControlPoint)other).GenerateTicks; } public override bool Equals(ControlPoint? other) @@ -193,10 +206,11 @@ namespace osu.Game.Beatmaps.Formats public bool Equals(LegacyDifficultyControlPoint? other) => base.Equals(other) - && BpmMultiplier == other.BpmMultiplier; + && BpmMultiplier == other.BpmMultiplier + && GenerateTicks == other.GenerateTicks; - // ReSharper disable once NonReadonlyMemberInGetHashCode - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), BpmMultiplier); + // ReSharper disable twice NonReadonlyMemberInGetHashCode + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), BpmMultiplier, GenerateTicks); } internal class LegacySampleControlPoint : SampleControlPoint, IEquatable From ada61e6fe7121960b2f531ff08bb29379ade63b1 Mon Sep 17 00:00:00 2001 From: Khang Date: Tue, 23 Aug 2022 14:10:26 -0400 Subject: [PATCH 293/532] Forgot to undo broken changes in JuiceStream --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 2a5564297c..311e15116e 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -11,7 +11,6 @@ using Newtonsoft.Json; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -51,12 +50,9 @@ namespace osu.Game.Rulesets.Catch.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); -#pragma warning disable 618 - bool generateTicks = (DifficultyControlPoint as LegacyBeatmapDecoder.LegacyDifficultyControlPoint)?.GenerateTicks ?? true; -#pragma warning restore 618 velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength; - tickDistanceFactor = generateTicks ? (base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate) : double.PositiveInfinity; + tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) From ec9daec93bcea47e7983564db3fb75b1c531733a Mon Sep 17 00:00:00 2001 From: Khang Date: Tue, 23 Aug 2022 14:18:40 -0400 Subject: [PATCH 294/532] Remove unnecessary workaround --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index df1252c060..a7495a2809 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -167,10 +167,8 @@ namespace osu.Game.Rulesets.Osu.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - - var legacyInfo = controlPointInfo as LegacyControlPointInfo; #pragma warning disable 618 - var legacyDifficultyPoint = legacyInfo?.DifficultyPointAt(StartTime) as LegacyBeatmapDecoder.LegacyDifficultyControlPoint; + var legacyDifficultyPoint = DifficultyControlPoint as LegacyBeatmapDecoder.LegacyDifficultyControlPoint; #pragma warning restore 618 double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; From 1f9cdff0139a84830cc099537b2cad7e9552c3f6 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 23 Aug 2022 22:19:40 +0200 Subject: [PATCH 295/532] remove these lines --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index d903ada6e2..de40d8e03b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -277,9 +277,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders // Turn the control points which were split off into a new slider. var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone(); - samplePoint.Time = HitObject.StartTime; var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone(); - difficultyPoint.Time = HitObject.StartTime; var newSlider = new Slider { From 631ea9a3edaf0ba8c27b685be971521a07931fbf Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 23 Aug 2022 23:28:50 +0200 Subject: [PATCH 296/532] added a gap between objects and made it theoretically possible to retain sample control point --- .../Editor/TestSceneSliderSplitting.cs | 72 +++++++++++++++++-- .../Sliders/SliderSelectionBlueprint.cs | 8 ++- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index 198d521a85..015952c59a 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -30,6 +31,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private Slider? slider; private PathControlPointVisualiser? visualiser; + private const double split_gap = 100; + [Test] public void TestBasicSplit() { @@ -69,11 +72,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addContextMenuItemStep("Split control point"); AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 2 && - sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, slider.StartTime, + sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap, (new Vector2(0, 50), PathType.PerfectCurve), (new Vector2(150, 200), null), (new Vector2(300, 50), null) - ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], slider.StartTime, endTime, + ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], slider.StartTime, endTime + split_gap, (new Vector2(300, 50), PathType.PerfectCurve), (new Vector2(400, 50), null), (new Vector2(400, 200), null) @@ -135,21 +138,80 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addContextMenuItemStep("Split 2 control points"); AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 3 && - sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime, + sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap, (new Vector2(0, 50), PathType.PerfectCurve), (new Vector2(150, 200), null), (new Vector2(300, 50), null) - ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], EditorBeatmap.HitObjects[0].GetEndTime(), slider.StartTime, + ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], EditorBeatmap.HitObjects[0].GetEndTime() + split_gap, slider.StartTime - split_gap, (new Vector2(300, 50), PathType.Bezier), (new Vector2(400, 50), null), (new Vector2(400, 200), null) - ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[2], EditorBeatmap.HitObjects[1].GetEndTime(), endTime, + ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[2], EditorBeatmap.HitObjects[1].GetEndTime() + split_gap, endTime + split_gap * 2, (new Vector2(400, 200), PathType.Catmull), (new Vector2(300, 250), null), (new Vector2(400, 300), null) )); } + [Test] + public void TestSplitRetainsHitsounds() + { + HitSampleInfo? sample = null; + + AddStep("add slider", () => + { + slider = new Slider + { + Position = new Vector2(0, 50), + LegacyLastTickOffset = 36, // This is necessary for undo to retain the sample control point + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(new Vector2(150, 150)), + new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(400, 0)), + new PathControlPoint(new Vector2(400, 150)) + }) + }; + + EditorBeatmap.Add(slider); + }); + + AddStep("add hitsounds", () => + { + if (slider is null) return; + + slider.SampleControlPoint.SampleBank = "soft"; + slider.SampleControlPoint.SampleVolume = 70; + sample = new HitSampleInfo("hitwhistle"); + slider.Samples.Add(sample); + }); + + AddStep("select added slider", () => + { + EditorBeatmap.SelectedHitObjects.Add(slider); + visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType().First(); + }); + + moveMouseToControlPoint(2); + AddStep("select control point", () => + { + if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true; + }); + addContextMenuItemStep("Split control point"); + AddAssert("sliders have hitsounds", hasHitsounds); + + AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0])); + AddStep("remove first slider", () => EditorBeatmap.RemoveAt(0)); + AddStep("undo", () => Editor.Undo()); + AddAssert("sliders have hitsounds", hasHitsounds); + + bool hasHitsounds() => sample is not null && + EditorBeatmap.HitObjects.All(o => o.SampleControlPoint.SampleBank == "soft" && + o.SampleControlPoint.SampleVolume == 70 && + o.Samples.Contains(sample)); + } + private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints) { if (!Precision.AlmostEquals(s.StartTime, startTime, 1) || !Precision.AlmostEquals(s.EndTime, endTime, 1)) return false; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index de40d8e03b..14ed305b25 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -253,6 +253,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void splitControlPoints(List toSplit) { + // Arbitrary gap in milliseconds to put between split slider pieces + const double split_gap = 100; + // Ensure that there are any points to be split if (toSplit.Count == 0) return; @@ -286,6 +289,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders NewCombo = HitObject.NewCombo, SampleControlPoint = samplePoint, DifficultyControlPoint = difficultyPoint, + LegacyLastTickOffset = HitObject.LegacyLastTickOffset, Samples = HitObject.Samples.Select(s => s.With()).ToList(), RepeatCount = HitObject.RepeatCount, NodeSamples = HitObject.NodeSamples.Select(n => (IList)n.Select(s => s.With()).ToList()).ToList(), @@ -293,13 +297,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders }; // Increase the start time of the slider before adding the new slider so the new slider is immediately inserted at the correct index and internal state remains valid. - HitObject.StartTime += 1; + HitObject.StartTime += split_gap; editorBeatmap.Add(newSlider); HitObject.NewCombo = false; HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance; - HitObject.StartTime += newSlider.SpanDuration - 1; + HitObject.StartTime += newSlider.SpanDuration; // In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider. if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON) From fb9bb2d42dd8884cfd4d93e15024d791a69e2eda Mon Sep 17 00:00:00 2001 From: vun Date: Wed, 24 Aug 2022 08:57:13 +0800 Subject: [PATCH 297/532] Declare Parent as non-nullable --- .../Difficulty/Evaluators/ColourEvaluator.cs | 4 ++-- .../Preprocessing/Colour/Data/AlternatingMonoPattern.cs | 2 +- .../Difficulty/Preprocessing/Colour/Data/MonoStreak.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 6c685e854e..7d88be2f70 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// public static double EvaluateDifficultyOf(MonoStreak monoStreak) { - return sigmoid(monoStreak.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(monoStreak.Parent!) * 0.5; + return sigmoid(monoStreak.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5; } /// @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern) { - return sigmoid(alternatingMonoPattern.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(alternatingMonoPattern.Parent!); + return sigmoid(alternatingMonoPattern.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(alternatingMonoPattern.Parent); } /// diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs index 60d4e55a64..7910a8262b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// /// The parent that contains this /// - public RepeatingHitPatterns? Parent; + public RepeatingHitPatterns Parent = null!; /// /// Index of this within it's parent diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs index 4e15043acf..174988bed7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data /// /// The parent that contains this /// - public AlternatingMonoPattern? Parent; + public AlternatingMonoPattern Parent = null!; /// /// Index of this within it's parent From 46d000b8cebb271f25e254c24b06a3c269ec71d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 11:50:19 +0900 Subject: [PATCH 298/532] Fix test regressions on windows due to `Reset` never being called I'm not sure this is great. Without calling `Reset`, the correct initial time may not be set (ever). In practice this doesn't happen anywhere in the gameplay flow, but something worth noting. This change is required now that `Reset` isn't called in the constructor. It couldn't be called in the constructor because it would cause the audio track to reset its position too early. What an ordeal. --- osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index dcaeadf82a..577933eae3 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Gameplay }); }); - AddStep("reset clock", () => gameplayContainer.Start()); + AddStep("reset clock", () => gameplayContainer.Reset(startClock: true)); AddUntilStep("sample played", () => sample.RequestedPlaying); AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue); @@ -147,7 +147,7 @@ namespace osu.Game.Tests.Gameplay }); }); - AddStep("start", () => gameplayContainer.Start()); + AddStep("reset clock", () => gameplayContainer.Reset(startClock: true)); AddUntilStep("sample played", () => sample.IsPlayed); AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue); From 85fbe7abca4274014c1fdfe4473ca053bda1337b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 13:11:53 +0900 Subject: [PATCH 299/532] Fix multiplayer spectator getting stuck --- .../Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs | 5 ++++- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs index 8d8f6a373a..5625a79afa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs @@ -73,7 +73,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate if (IsRunning) { - double elapsedSource = Source.ElapsedFrameTime; + // When in catch-up mode, the source is usually not running. + // In such a case, its elapsed time may be zero, which would cause catch-up to get stuck. + // To avoid this, use a constant 16ms elapsed time for now. Probably not too correct, but this whole logic isn't too correct anyway. + double elapsedSource = Source.IsRunning ? Source.ElapsedFrameTime : 16; double elapsed = elapsedSource * Rate; CurrentTime += elapsed; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 2a2575e4b2..953d16f6e8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; @@ -195,6 +196,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private void onMasterStateChanged(ValueChangedEvent state) { + Logger.Log($"{nameof(MultiSpectatorScreen)}'s master clock become {state.NewValue}"); + switch (state.NewValue) { case MasterClockState.Synchronised: From ec31f37ff7a1c436c054b6e963c0d4f2fa71b9a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 14:50:10 +0900 Subject: [PATCH 300/532] Accept `MasterGameplayClockContainer` rather than generic clock --- .../Spectate/CatchUpSpectatorPlayerClock.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs index 8d8f6a373a..c0263d6d0f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Timing; +using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { @@ -17,15 +18,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public const double CATCHUP_RATE = 2; - public readonly IFrameBasedClock Source; + private readonly GameplayClockContainer masterClock; public double CurrentTime { get; private set; } public bool IsRunning { get; private set; } - public CatchUpSpectatorPlayerClock(IFrameBasedClock source) + public CatchUpSpectatorPlayerClock(GameplayClockContainer masterClock) { - Source = source; + this.masterClock = masterClock; } public void Reset() => CurrentTime = 0; @@ -69,16 +70,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate ElapsedFrameTime = 0; FramesPerSecond = 0; - Source.ProcessFrame(); + masterClock.ProcessFrame(); if (IsRunning) { - double elapsedSource = Source.ElapsedFrameTime; + double elapsedSource = masterClock.ElapsedFrameTime; double elapsed = elapsedSource * Rate; CurrentTime += elapsed; ElapsedFrameTime = elapsed; - FramesPerSecond = Source.FramesPerSecond; + FramesPerSecond = masterClock.FramesPerSecond; } } From c9f364d6a086c7582040c9b66a94eca09e8ce25e Mon Sep 17 00:00:00 2001 From: Khang Date: Wed, 24 Aug 2022 02:10:19 -0400 Subject: [PATCH 301/532] Document why beatLength can be NaN --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index f064d89e19..75500fbc4e 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -373,6 +373,8 @@ namespace osu.Game.Beatmaps.Formats string[] split = line.Split(','); double time = getOffsetTime(Parsing.ParseDouble(split[0].Trim())); + + // beatLength is allowed to be NaN to handle an edge case in which some beatmaps use NaN slider velocity to disable slider tick generation (see LegacyDifficultyControlPoint). double beatLength = Parsing.ParseDouble(split[1].Trim(), allowNaN: true); // If beatLength is NaN, speedMultiplier should still be 1 because all comparisons against NaN are false. From 22963ab95162fc13770b128c09e7b8c57b061a87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 13:11:53 +0900 Subject: [PATCH 302/532] Fix multiplayer spectator getting stuck --- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 3d04ae8f3c..0f0f29e02e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; @@ -198,6 +199,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private void onMasterStateChanged(ValueChangedEvent state) { + Logger.Log($"{nameof(MultiSpectatorScreen)}'s master clock become {state.NewValue}"); + switch (state.NewValue) { case MasterClockState.Synchronised: From 882dd93942356d2122f4c34fe7d41a97bb98c33e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:01:57 +0900 Subject: [PATCH 303/532] Remove `ISyncManager` interface Too many levels of redirection. One interface with one implementation is not useful, IMO. --- .../Spectate/CatchUpSyncManager.cs | 18 +++++++- .../Spectate/ISpectatorPlayerClock.cs | 2 +- .../Multiplayer/Spectate/ISyncManager.cs | 42 ------------------- .../Spectate/MultiSpectatorScreen.cs | 2 +- 4 files changed, 18 insertions(+), 46 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs index 4e563ec69a..b9898ad456 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs @@ -11,9 +11,9 @@ using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { /// - /// A which synchronises de-synced player clocks through catchup. + /// Manages the synchronisation between one or more s in relation to a master clock. /// - public class CatchUpSyncManager : Component, ISyncManager + public class CatchUpSyncManager : Component { /// /// The offset from the master clock to which player clocks should remain within to be considered in-sync. @@ -30,6 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public const double MAXIMUM_START_DELAY = 15000; + /// + /// An event which is invoked when gameplay is ready to start. + /// public event Action? ReadyToStart; /// @@ -37,6 +40,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public GameplayClockContainer MasterClock { get; } + /// + /// The catch-up state of the master clock. + /// public IBindable MasterState => masterState; /// @@ -54,6 +60,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate MasterClock = master; } + /// + /// Create a new managed . + /// + /// The newly created . public ISpectatorPlayerClock CreateManagedClock() { var clock = new CatchUpSpectatorPlayerClock(MasterClock); @@ -61,6 +71,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return clock; } + /// + /// Removes an , stopping it from being managed by this . + /// + /// The to remove. public void RemoveManagedClock(ISpectatorPlayerClock clock) { playerClocks.Remove(clock); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs index a2e6df9282..22f835f79e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs @@ -7,7 +7,7 @@ using osu.Framework.Timing; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { /// - /// A clock which is used by s and managed by an . + /// A clock which is used by s and managed by an . /// public interface ISpectatorPlayerClock : IFrameBasedClock, IAdjustableClock { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs deleted file mode 100644 index 5615e02336..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Bindables; -using osu.Game.Screens.Play; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate -{ - /// - /// Manages the synchronisation between one or more s in relation to a master clock. - /// - public interface ISyncManager - { - /// - /// An event which is invoked when gameplay is ready to start. - /// - event Action? ReadyToStart; - - /// - /// The master clock which player clocks should synchronise to. - /// - GameplayClockContainer MasterClock { get; } - - /// - /// An event which is invoked when the state of is changed. - /// - IBindable MasterState { get; } - - /// - /// Create a new managed . - /// - /// The newly created . - ISpectatorPlayerClock CreateManagedClock(); - - /// - /// Removes an , stopping it from being managed by this . - /// - /// The to remove. - void RemoveManagedClock(ISpectatorPlayerClock clock); - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 0f0f29e02e..0177003bb2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly PlayerArea[] instances; private MasterGameplayClockContainer masterClockContainer = null!; - private ISyncManager syncManager = null!; + private CatchUpSyncManager syncManager = null!; private PlayerGrid grid = null!; private MultiSpectatorLeaderboard leaderboard = null!; private PlayerArea? currentAudioSource; From 31f657fe01f7b7ce53b7ef3fa07fed91bb4502ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:04:52 +0900 Subject: [PATCH 304/532] Remove `ISpectatorPlayerClock` interface Too many levels of redirection. One interface with one implementation is not useful, IMO. --- .../OnlinePlay/TestSceneCatchUpSyncManager.cs | 16 ++++---- .../Spectate/CatchUpSpectatorPlayerClock.cs | 19 +++++++++- .../Spectate/CatchUpSyncManager.cs | 16 ++++---- .../Spectate/ISpectatorPlayerClock.cs | 37 ------------------- .../Spectate/MultiSpectatorPlayer.cs | 4 +- .../Spectate/MultiSpectatorScreen.cs | 2 +- .../Multiplayer/Spectate/PlayerArea.cs | 6 +-- 7 files changed, 39 insertions(+), 61 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index db14dc95b2..263ce7c9bd 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -21,9 +21,9 @@ namespace osu.Game.Tests.OnlinePlay private GameplayClockContainer master; private CatchUpSyncManager syncManager; - private Dictionary clocksById; - private ISpectatorPlayerClock player1; - private ISpectatorPlayerClock player2; + private Dictionary clocksById; + private CatchUpSpectatorPlayerClock player1; + private CatchUpSpectatorPlayerClock player2; [SetUp] public void Setup() @@ -32,7 +32,7 @@ namespace osu.Game.Tests.OnlinePlay player1 = syncManager.CreateManagedClock(); player2 = syncManager.CreateManagedClock(); - clocksById = new Dictionary + clocksById = new Dictionary { { player1, 1 }, { player2, 2 } @@ -145,7 +145,7 @@ namespace osu.Game.Tests.OnlinePlay assertPlayerClockState(() => player1, false); } - private void setWaiting(Func playerClock, bool waiting) + private void setWaiting(Func playerClock, bool waiting) => AddStep($"set player clock {clocksById[playerClock()]} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = waiting); private void setAllWaiting(bool waiting) => AddStep($"set all player clocks waiting = {waiting}", () => @@ -160,13 +160,13 @@ namespace osu.Game.Tests.OnlinePlay /// /// clock.Time = master.Time - offsetFromMaster /// - private void setPlayerClockTime(Func playerClock, double offsetFromMaster) + private void setPlayerClockTime(Func playerClock, double offsetFromMaster) => AddStep($"set player clock {clocksById[playerClock()]} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster)); - private void assertCatchingUp(Func playerClock, bool catchingUp) => + private void assertCatchingUp(Func playerClock, bool catchingUp) => AddAssert($"player clock {clocksById[playerClock()]} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp); - private void assertPlayerClockState(Func playerClock, bool running) + private void assertPlayerClockState(Func playerClock, bool running) => AddAssert($"player clock {clocksById[playerClock()]} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running); private class TestManualClock : ManualClock, IAdjustableClock diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs index c0263d6d0f..15821fb133 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs @@ -9,9 +9,9 @@ using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { /// - /// A which catches up using rate adjustment. + /// A which catches up using rate adjustment. /// - public class CatchUpSpectatorPlayerClock : ISpectatorPlayerClock + public class CatchUpSpectatorPlayerClock : IFrameBasedClock, IAdjustableClock { /// /// The catch up rate. @@ -31,8 +31,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public void Reset() => CurrentTime = 0; + /// + /// Starts this . + /// public void Start() => IsRunning = true; + /// + /// Stops this . + /// public void Stop() => IsRunning = false; void IAdjustableClock.Start() @@ -89,8 +95,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime }; + /// + /// Whether this clock is waiting on frames to continue playback. + /// public Bindable WaitingOnFrames { get; } = new Bindable(true); + /// + /// Whether this clock is behind the master clock and running at a higher rate to catch up to it. + /// + /// + /// Of note, this will be false if this clock is *ahead* of the master clock. + /// public bool IsCatchingUp { get; set; } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs index b9898ad456..71fbb8cbee 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs @@ -11,7 +11,7 @@ using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { /// - /// Manages the synchronisation between one or more s in relation to a master clock. + /// Manages the synchronisation between one or more s in relation to a master clock. /// public class CatchUpSyncManager : Component { @@ -48,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// The player clocks. /// - private readonly List playerClocks = new List(); + private readonly List playerClocks = new List(); private readonly Bindable masterState = new Bindable(); @@ -61,10 +61,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } /// - /// Create a new managed . + /// Create a new managed . /// - /// The newly created . - public ISpectatorPlayerClock CreateManagedClock() + /// The newly created . + public CatchUpSpectatorPlayerClock CreateManagedClock() { var clock = new CatchUpSpectatorPlayerClock(MasterClock); playerClocks.Add(clock); @@ -72,10 +72,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } /// - /// Removes an , stopping it from being managed by this . + /// Removes an , stopping it from being managed by this . /// - /// The to remove. - public void RemoveManagedClock(ISpectatorPlayerClock clock) + /// The to remove. + public void RemoveManagedClock(CatchUpSpectatorPlayerClock clock) { playerClocks.Remove(clock); clock.Stop(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs deleted file mode 100644 index 22f835f79e..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Bindables; -using osu.Framework.Timing; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate -{ - /// - /// A clock which is used by s and managed by an . - /// - public interface ISpectatorPlayerClock : IFrameBasedClock, IAdjustableClock - { - /// - /// Starts this . - /// - new void Start(); - - /// - /// Stops this . - /// - new void Stop(); - - /// - /// Whether this clock is waiting on frames to continue playback. - /// - Bindable WaitingOnFrames { get; } - - /// - /// Whether this clock is behind the master clock and running at a higher rate to catch up to it. - /// - /// - /// Of note, this will be false if this clock is *ahead* of the master clock. - /// - bool IsCatchingUp { get; set; } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 68eae76030..4821eb69c6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -15,14 +15,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public class MultiSpectatorPlayer : SpectatorPlayer { private readonly Bindable waitingOnFrames = new Bindable(true); - private readonly ISpectatorPlayerClock spectatorPlayerClock; + private readonly CatchUpSpectatorPlayerClock spectatorPlayerClock; /// /// Creates a new . /// /// The score containing the player's replay. /// The clock controlling the gameplay running state. - public MultiSpectatorPlayer(Score score, ISpectatorPlayerClock spectatorPlayerClock) + public MultiSpectatorPlayer(Score score, CatchUpSpectatorPlayerClock spectatorPlayerClock) : base(score, new PlayerConfiguration { AllowUserInteraction = false }) { this.spectatorPlayerClock = spectatorPlayerClock; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 0177003bb2..ac66aa160c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -177,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } } - private bool isCandidateAudioSource(ISpectatorPlayerClock? clock) + private bool isCandidateAudioSource(CatchUpSpectatorPlayerClock? clock) => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value; private void onReadyToStart() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 7e679383c4..451f4e0b79 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -38,9 +38,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public readonly int UserId; /// - /// The used to control the gameplay running state of a loaded . + /// The used to control the gameplay running state of a loaded . /// - public readonly ISpectatorPlayerClock GameplayClock; + public readonly CatchUpSpectatorPlayerClock GameplayClock; /// /// The currently-loaded score. @@ -55,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly LoadingLayer loadingLayer; private OsuScreenStack? stack; - public PlayerArea(int userId, ISpectatorPlayerClock clock) + public PlayerArea(int userId, CatchUpSpectatorPlayerClock clock) { UserId = userId; GameplayClock = clock; From 995e6664b612aac7d2c8565b8ab1a839c227bab8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:07:04 +0900 Subject: [PATCH 305/532] Rename spectator clock sync classes --- .../OnlinePlay/TestSceneCatchUpSyncManager.cs | 38 +++++++++---------- .../TestSceneMultiSpectatorScreen.cs | 4 +- .../Spectate/MultiSpectatorPlayer.cs | 8 ++-- .../Spectate/MultiSpectatorScreen.cs | 6 +-- .../Multiplayer/Spectate/PlayerArea.cs | 6 +-- ...PlayerClock.cs => SpectatorPlayerClock.cs} | 10 ++--- ...SyncManager.cs => SpectatorSyncManager.cs} | 22 +++++------ 7 files changed, 47 insertions(+), 47 deletions(-) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{CatchUpSpectatorPlayerClock.cs => SpectatorPlayerClock.cs} (88%) rename osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/{CatchUpSyncManager.cs => SpectatorSyncManager.cs} (86%) diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index 263ce7c9bd..5761a89ae8 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -19,20 +19,20 @@ namespace osu.Game.Tests.OnlinePlay public class TestSceneCatchUpSyncManager : OsuTestScene { private GameplayClockContainer master; - private CatchUpSyncManager syncManager; + private SpectatorSyncManager syncManager; - private Dictionary clocksById; - private CatchUpSpectatorPlayerClock player1; - private CatchUpSpectatorPlayerClock player2; + private Dictionary clocksById; + private SpectatorPlayerClock player1; + private SpectatorPlayerClock player2; [SetUp] public void Setup() { - syncManager = new CatchUpSyncManager(master = new GameplayClockContainer(new TestManualClock())); + syncManager = new SpectatorSyncManager(master = new GameplayClockContainer(new TestManualClock())); player1 = syncManager.CreateManagedClock(); player2 = syncManager.CreateManagedClock(); - clocksById = new Dictionary + clocksById = new Dictionary { { player1, 1 }, { player2, 2 } @@ -64,7 +64,7 @@ namespace osu.Game.Tests.OnlinePlay public void TestReadyPlayersStartWhenReadyForMaximumDelayTime() { setWaiting(() => player1, false); - AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + AddWaitStep($"wait {SpectatorSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction)); assertPlayerClockState(() => player1, true); assertPlayerClockState(() => player2, false); } @@ -74,7 +74,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(CatchUpSyncManager.SYNC_TARGET + 1); + setMasterTime(SpectatorSyncManager.SYNC_TARGET + 1); assertCatchingUp(() => player1, false); } @@ -83,7 +83,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1); + setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 1); assertCatchingUp(() => player1, true); assertCatchingUp(() => player2, true); } @@ -93,8 +93,8 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1); - setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET + 1); + setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 1); + setPlayerClockTime(() => player1, SpectatorSyncManager.SYNC_TARGET + 1); assertCatchingUp(() => player1, true); } @@ -103,8 +103,8 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 2); - setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET); + setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 2); + setPlayerClockTime(() => player1, SpectatorSyncManager.SYNC_TARGET); assertCatchingUp(() => player1, false); assertCatchingUp(() => player2, true); } @@ -114,7 +114,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET); + setPlayerClockTime(() => player1, -SpectatorSyncManager.SYNC_TARGET); assertCatchingUp(() => player1, false); assertPlayerClockState(() => player1, true); } @@ -124,7 +124,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET - 1); + setPlayerClockTime(() => player1, -SpectatorSyncManager.SYNC_TARGET - 1); // This is a silent catchup, where IsCatchingUp = false but IsRunning = false also. assertCatchingUp(() => player1, false); @@ -145,7 +145,7 @@ namespace osu.Game.Tests.OnlinePlay assertPlayerClockState(() => player1, false); } - private void setWaiting(Func playerClock, bool waiting) + private void setWaiting(Func playerClock, bool waiting) => AddStep($"set player clock {clocksById[playerClock()]} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = waiting); private void setAllWaiting(bool waiting) => AddStep($"set all player clocks waiting = {waiting}", () => @@ -160,13 +160,13 @@ namespace osu.Game.Tests.OnlinePlay /// /// clock.Time = master.Time - offsetFromMaster /// - private void setPlayerClockTime(Func playerClock, double offsetFromMaster) + private void setPlayerClockTime(Func playerClock, double offsetFromMaster) => AddStep($"set player clock {clocksById[playerClock()]} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster)); - private void assertCatchingUp(Func playerClock, bool catchingUp) => + private void assertCatchingUp(Func playerClock, bool catchingUp) => AddAssert($"player clock {clocksById[playerClock()]} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp); - private void assertPlayerClockState(Func playerClock, bool running) + private void assertPlayerClockState(Func playerClock, bool running) => AddAssert($"player clock {clocksById[playerClock()]} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running); private class TestManualClock : ManualClock, IAdjustableClock diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index a2e3ab7318..bab613bed7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -202,7 +202,7 @@ namespace osu.Game.Tests.Visual.Multiplayer checkPausedInstant(PLAYER_2_ID, true); // Wait for the start delay seconds... - AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + AddWaitStep("wait maximum start delay seconds", (int)(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction)); // Player 1 should start playing by itself, player 2 should remain paused. checkPausedInstant(PLAYER_1_ID, false); @@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Multiplayer loadSpectateScreen(); sendFrames(PLAYER_1_ID, 300); - AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + AddWaitStep("wait maximum start delay seconds", (int)(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction)); checkPaused(PLAYER_1_ID, false); sendFrames(PLAYER_2_ID, 300); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 4821eb69c6..8445a6fdf0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -15,14 +15,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public class MultiSpectatorPlayer : SpectatorPlayer { private readonly Bindable waitingOnFrames = new Bindable(true); - private readonly CatchUpSpectatorPlayerClock spectatorPlayerClock; + private readonly SpectatorPlayerClock spectatorPlayerClock; /// /// Creates a new . /// /// The score containing the player's replay. /// The clock controlling the gameplay running state. - public MultiSpectatorPlayer(Score score, CatchUpSpectatorPlayerClock spectatorPlayerClock) + public MultiSpectatorPlayer(Score score, SpectatorPlayerClock spectatorPlayerClock) : base(score, new PlayerConfiguration { AllowUserInteraction = false }) { this.spectatorPlayerClock = spectatorPlayerClock; @@ -40,9 +40,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void Update() { // The player clock's running state is controlled externally, but the local pausing state needs to be updated to start/stop gameplay. - CatchUpSpectatorPlayerClock catchUpClock = (CatchUpSpectatorPlayerClock)GameplayClockContainer.SourceClock; + SpectatorPlayerClock clock = (SpectatorPlayerClock)GameplayClockContainer.SourceClock; - if (catchUpClock.IsRunning) + if (clock.IsRunning) GameplayClockContainer.Start(); else GameplayClockContainer.Stop(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index ac66aa160c..8af5a640fa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly PlayerArea[] instances; private MasterGameplayClockContainer masterClockContainer = null!; - private CatchUpSyncManager syncManager = null!; + private SpectatorSyncManager syncManager = null!; private PlayerGrid grid = null!; private MultiSpectatorLeaderboard leaderboard = null!; private PlayerArea? currentAudioSource; @@ -81,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate InternalChildren = new[] { - (Drawable)(syncManager = new CatchUpSyncManager(masterClockContainer)), + (Drawable)(syncManager = new SpectatorSyncManager(masterClockContainer)), masterClockContainer.WithChild(new GridContainer { RelativeSizeAxes = Axes.Both, @@ -177,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } } - private bool isCandidateAudioSource(CatchUpSpectatorPlayerClock? clock) + private bool isCandidateAudioSource(SpectatorPlayerClock? clock) => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value; private void onReadyToStart() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 451f4e0b79..a1fbdc10de 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -38,9 +38,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public readonly int UserId; /// - /// The used to control the gameplay running state of a loaded . + /// The used to control the gameplay running state of a loaded . /// - public readonly CatchUpSpectatorPlayerClock GameplayClock; + public readonly SpectatorPlayerClock GameplayClock; /// /// The currently-loaded score. @@ -55,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly LoadingLayer loadingLayer; private OsuScreenStack? stack; - public PlayerArea(int userId, CatchUpSpectatorPlayerClock clock) + public PlayerArea(int userId, SpectatorPlayerClock clock) { UserId = userId; GameplayClock = clock; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs similarity index 88% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs index 15821fb133..729a120dc1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs @@ -9,9 +9,9 @@ using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { /// - /// A which catches up using rate adjustment. + /// A clock which catches up using rate adjustment. /// - public class CatchUpSpectatorPlayerClock : IFrameBasedClock, IAdjustableClock + public class SpectatorPlayerClock : IFrameBasedClock, IAdjustableClock { /// /// The catch up rate. @@ -24,7 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public bool IsRunning { get; private set; } - public CatchUpSpectatorPlayerClock(GameplayClockContainer masterClock) + public SpectatorPlayerClock(GameplayClockContainer masterClock) { this.masterClock = masterClock; } @@ -32,12 +32,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public void Reset() => CurrentTime = 0; /// - /// Starts this . + /// Starts this . /// public void Start() => IsRunning = true; /// - /// Stops this . + /// Stops this . /// public void Stop() => IsRunning = false; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs similarity index 86% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs index 71fbb8cbee..d2f2efffc9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs @@ -11,9 +11,9 @@ using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { /// - /// Manages the synchronisation between one or more s in relation to a master clock. + /// Manages the synchronisation between one or more s in relation to a master clock. /// - public class CatchUpSyncManager : Component + public class SpectatorSyncManager : Component { /// /// The offset from the master clock to which player clocks should remain within to be considered in-sync. @@ -48,34 +48,34 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// The player clocks. /// - private readonly List playerClocks = new List(); + private readonly List playerClocks = new List(); private readonly Bindable masterState = new Bindable(); private bool hasStarted; private double? firstStartAttemptTime; - public CatchUpSyncManager(GameplayClockContainer master) + public SpectatorSyncManager(GameplayClockContainer master) { MasterClock = master; } /// - /// Create a new managed . + /// Create a new managed . /// - /// The newly created . - public CatchUpSpectatorPlayerClock CreateManagedClock() + /// The newly created . + public SpectatorPlayerClock CreateManagedClock() { - var clock = new CatchUpSpectatorPlayerClock(MasterClock); + var clock = new SpectatorPlayerClock(MasterClock); playerClocks.Add(clock); return clock; } /// - /// Removes an , stopping it from being managed by this . + /// Removes an , stopping it from being managed by this . /// - /// The to remove. - public void RemoveManagedClock(CatchUpSpectatorPlayerClock clock) + /// The to remove. + public void RemoveManagedClock(SpectatorPlayerClock clock) { playerClocks.Remove(clock); clock.Stop(); From 0c9a4ec13cab46f49bd04da9b03897a3c523cc16 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:09:00 +0900 Subject: [PATCH 306/532] Don't expose `MasterClock` in `SpectatorClockSyncManager` --- .../Spectate/MultiSpectatorScreen.cs | 2 +- .../Spectate/SpectatorSyncManager.cs | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 8af5a640fa..b285d3d7c2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -169,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate if (!isCandidateAudioSource(currentAudioSource?.GameplayClock)) { currentAudioSource = instances.Where(i => isCandidateAudioSource(i.GameplayClock)) - .OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.MasterClock.CurrentTime)) + .OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.CurrentMasterTime)) .FirstOrDefault(); foreach (var instance in instances) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs index d2f2efffc9..aa6fb878b3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs @@ -35,16 +35,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public event Action? ReadyToStart; - /// - /// The master clock which is used to control the timing of all player clocks clocks. - /// - public GameplayClockContainer MasterClock { get; } - /// /// The catch-up state of the master clock. /// public IBindable MasterState => masterState; + public double CurrentMasterTime => masterClock.CurrentTime; + + /// + /// The master clock which is used to control the timing of all player clocks clocks. + /// + private GameplayClockContainer masterClock { get; } + /// /// The player clocks. /// @@ -57,7 +59,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public SpectatorSyncManager(GameplayClockContainer master) { - MasterClock = master; + masterClock = master; } /// @@ -66,7 +68,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// The newly created . public SpectatorPlayerClock CreateManagedClock() { - var clock = new SpectatorPlayerClock(MasterClock); + var clock = new SpectatorPlayerClock(masterClock); playerClocks.Add(clock); return clock; } @@ -142,7 +144,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate // How far this player's clock is out of sync, compared to the master clock. // A negative value means the player is running fast (ahead); a positive value means the player is running behind (catching up). - double timeDelta = MasterClock.CurrentTime - clock.CurrentTime; + double timeDelta = masterClock.CurrentTime - clock.CurrentTime; // Check that the player clock isn't too far ahead. // This is a quiet case in which the catchup is done by the master clock, so IsCatchingUp is not set on the player clock. From a86fc6f248b3c4712a9542409de54c8608d973a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:17:56 +0900 Subject: [PATCH 307/532] Change running state of `SpectatorPlayerClock` using `IsRunning` --- .../Spectate/SpectatorPlayerClock.cs | 50 ++++++++----------- .../Spectate/SpectatorSyncManager.cs | 11 ++-- 2 files changed, 26 insertions(+), 35 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs index 729a120dc1..4e785ad3b1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs @@ -22,7 +22,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public double CurrentTime { get; private set; } - public bool IsRunning { get; private set; } + /// + /// Whether this clock is waiting on frames to continue playback. + /// + public Bindable WaitingOnFrames { get; } = new Bindable(true); + + /// + /// Whether this clock is behind the master clock and running at a higher rate to catch up to it. + /// + /// + /// Of note, this will be false if this clock is *ahead* of the master clock. + /// + public bool IsCatchingUp { get; set; } + + /// + /// Whether this spectator clock should be running. + /// Use instead of / to control time. + /// + public bool IsRunning { get; set; } public SpectatorPlayerClock(GameplayClockContainer masterClock) { @@ -31,24 +48,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public void Reset() => CurrentTime = 0; - /// - /// Starts this . - /// - public void Start() => IsRunning = true; - - /// - /// Stops this . - /// - public void Stop() => IsRunning = false; - - void IAdjustableClock.Start() + public void Start() { - // Our running state should only be managed by an ISyncManager, ignore calls from external sources. + // Our running state should only be managed by SpectatorSyncManager via IsRunning. } - void IAdjustableClock.Stop() + public void Stop() { - // Our running state should only be managed by an ISyncManager, ignore calls from external sources. + // Our running state should only be managed by an SpectatorSyncManager via IsRunning. } public bool Seek(double position) @@ -94,18 +101,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public double FramesPerSecond { get; private set; } public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime }; - - /// - /// Whether this clock is waiting on frames to continue playback. - /// - public Bindable WaitingOnFrames { get; } = new Bindable(true); - - /// - /// Whether this clock is behind the master clock and running at a higher rate to catch up to it. - /// - /// - /// Of note, this will be false if this clock is *ahead* of the master clock. - /// - public bool IsCatchingUp { get; set; } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs index aa6fb878b3..7ec8f45b1f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public void RemoveManagedClock(SpectatorPlayerClock clock) { playerClocks.Remove(clock); - clock.Stop(); + clock.IsRunning = false; } protected override void Update() @@ -91,7 +91,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { // Ensure all player clocks are stopped until the start succeeds. foreach (var clock in playerClocks) - clock.Stop(); + clock.IsRunning = true; return; } @@ -153,15 +153,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate // Importantly, set the clock to a non-catchup state. if this isn't done, updateMasterState may incorrectly pause the master clock // when it is required to be running (ie. if all players are ahead of the master). clock.IsCatchingUp = false; - clock.Stop(); + clock.IsRunning = false; continue; } // Make sure the player clock is running if it can. - if (!clock.WaitingOnFrames.Value) - clock.Start(); - else - clock.Stop(); + clock.IsRunning = !clock.WaitingOnFrames.Value; if (clock.IsCatchingUp) { From b6254a1f252f55750cb66a9033f8e65719b35291 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:23:31 +0900 Subject: [PATCH 308/532] Remove unnecessary casting --- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 8445a6fdf0..57af929cb7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -40,9 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void Update() { // The player clock's running state is controlled externally, but the local pausing state needs to be updated to start/stop gameplay. - SpectatorPlayerClock clock = (SpectatorPlayerClock)GameplayClockContainer.SourceClock; - - if (clock.IsRunning) + if (GameplayClockContainer.SourceClock.IsRunning) GameplayClockContainer.Start(); else GameplayClockContainer.Stop(); From 0b271fe4b3ae9a9e4aa78cb138136a37bcba342c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:27:31 +0900 Subject: [PATCH 309/532] Fix incorrect `IsRunning` value --- .../OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs index 7ec8f45b1f..c7b70cc6c7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs @@ -91,7 +91,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { // Ensure all player clocks are stopped until the start succeeds. foreach (var clock in playerClocks) - clock.IsRunning = true; + clock.IsRunning = false; return; } From b4eede61fb6c1b2dbfd13e262d9c90c1ac42400f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:28:18 +0900 Subject: [PATCH 310/532] Use `readonly` instead of `get-only` --- .../OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs index c7b70cc6c7..119e82a682 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// The master clock which is used to control the timing of all player clocks clocks. /// - private GameplayClockContainer masterClock { get; } + private readonly GameplayClockContainer masterClock; /// /// The player clocks. From c7d4c739aa067825e89a97f11a8d3ae3e4965602 Mon Sep 17 00:00:00 2001 From: Khang Date: Wed, 24 Aug 2022 02:53:55 -0400 Subject: [PATCH 311/532] Add a basic NaN control point test for LegacyBeatmapDecoder --- .../Formats/LegacyBeatmapDecoderTest.cs | 27 +++++++++++++++++++ .../Resources/nan-control-points.osu | 15 +++++++++++ 2 files changed, 42 insertions(+) create mode 100644 osu.Game.Tests/Resources/nan-control-points.osu diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 9acd3a6cab..7bd32eb5bd 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -919,5 +919,32 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(controlPoints[1].Position, Is.Not.EqualTo(Vector2.Zero)); } } + + [Test] + public void TestNaNControlPoints() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("nan-control-points.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo; + + Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(1)); + Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(3)); + + Assert.That(controlPoints.TimingPointAt(1000).BeatLength, Is.EqualTo(500)); + + Assert.That(controlPoints.DifficultyPointAt(1000).SliderVelocity, Is.EqualTo(1)); + Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1)); + Assert.That(controlPoints.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(1)); + +#pragma warning disable 618 + Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(1000)).GenerateTicks, Is.True); + Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(2000)).GenerateTicks, Is.False); + Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(3000)).GenerateTicks, Is.True); +#pragma warning restore 618 + } + } } } diff --git a/osu.Game.Tests/Resources/nan-control-points.osu b/osu.Game.Tests/Resources/nan-control-points.osu new file mode 100644 index 0000000000..dcaa705116 --- /dev/null +++ b/osu.Game.Tests/Resources/nan-control-points.osu @@ -0,0 +1,15 @@ +osu file format v14 + +[TimingPoints] + +// NaN bpm (should be rejected) +0,NaN,4,2,0,100,1,0 + +// 120 bpm +1000,500,4,2,0,100,1,0 + +// NaN slider velocity +2000,NaN,4,3,0,100,0,1 + +// 1.0x slider velocity +3000,-100,4,3,0,100,0,1 \ No newline at end of file From 9c6968e96dfb322cbcc08a17dcd5ed0dbaf6ea00 Mon Sep 17 00:00:00 2001 From: Khang Date: Wed, 24 Aug 2022 02:54:40 -0400 Subject: [PATCH 312/532] Remove unused import in Slider --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index a7495a2809..e3c1b1e168 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -15,7 +15,6 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; -using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; From 7e5086c8d79dd1c440a30dfde936ae496b0a1ebf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 24 Aug 2022 09:50:29 +0300 Subject: [PATCH 313/532] Fix spectator client not handling multiple watch calls properly --- osu.Game/Online/Spectator/SpectatorClient.cs | 32 ++++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 745c968992..f1ce6258d6 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -65,11 +65,12 @@ namespace osu.Game.Online.Spectator public virtual event Action? OnUserFinishedPlaying; /// - /// All users currently being watched. + /// A dictionary containing all users currently being watched, with the number of watching components for each user. /// - private readonly List watchedUsers = new List(); + private readonly Dictionary watchedUsers = new Dictionary(); private readonly BindableDictionary watchedUserStates = new BindableDictionary(); + private readonly BindableList playingUsers = new BindableList(); private readonly SpectatorState currentState = new SpectatorState(); @@ -94,12 +95,15 @@ namespace osu.Game.Online.Spectator if (connected.NewValue) { // get all the users that were previously being watched - int[] users = watchedUsers.ToArray(); + var users = new Dictionary(watchedUsers); watchedUsers.Clear(); // resubscribe to watched users. - foreach (int userId in users) - WatchUser(userId); + foreach ((int user, int watchers) in users) + { + for (int i = 0; i < watchers; i++) + WatchUser(user); + } // re-send state in case it wasn't received if (IsPlaying) @@ -121,7 +125,7 @@ namespace osu.Game.Online.Spectator if (!playingUsers.Contains(userId)) playingUsers.Add(userId); - if (watchedUsers.Contains(userId)) + if (watchedUsers.ContainsKey(userId)) watchedUserStates[userId] = state; OnUserBeganPlaying?.Invoke(userId, state); @@ -136,7 +140,7 @@ namespace osu.Game.Online.Spectator { playingUsers.Remove(userId); - if (watchedUsers.Contains(userId)) + if (watchedUsers.ContainsKey(userId)) watchedUserStates[userId] = state; OnUserFinishedPlaying?.Invoke(userId, state); @@ -232,11 +236,13 @@ namespace osu.Game.Online.Spectator { Debug.Assert(ThreadSafety.IsUpdateThread); - if (watchedUsers.Contains(userId)) + if (watchedUsers.ContainsKey(userId)) + { + watchedUsers[userId]++; return; + } - watchedUsers.Add(userId); - + watchedUsers.Add(userId, 1); WatchUserInternal(userId); } @@ -246,6 +252,12 @@ namespace osu.Game.Online.Spectator // Todo: This should not be a thing, but requires framework changes. Schedule(() => { + if (watchedUsers.TryGetValue(userId, out int watchers) && watchers > 1) + { + watchedUsers[userId]--; + return; + } + watchedUsers.Remove(userId); watchedUserStates.Remove(userId); StopWatchingUserInternal(userId); From 2fa8b61f3c0bb3f5065aa6f3cc2ad7dfb6f43d2e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 6 Aug 2022 05:51:57 +0300 Subject: [PATCH 314/532] Handle completion user state updates during spectating --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 0c1fe56eb6..8218ddf641 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -115,13 +115,13 @@ namespace osu.Game.Screens.Spectate { case NotifyDictionaryChangedAction.Add: case NotifyDictionaryChangedAction.Replace: - foreach ((int userId, var state) in e.NewItems.AsNonNull()) + foreach ((int userId, SpectatorState state) in e.NewItems.AsNonNull()) onUserStateChanged(userId, state); break; case NotifyDictionaryChangedAction.Remove: foreach ((int userId, SpectatorState state) in e.OldItems.AsNonNull()) - onUserStateRemoved(userId, state); + onUserStateChanged(userId, state); break; } } @@ -136,33 +136,19 @@ namespace osu.Game.Screens.Spectate switch (newState.State) { - case SpectatedUserState.Passed: - // Make sure that gameplay completes to the end. - if (gameplayStates.TryGetValue(userId, out var gameplayState)) - gameplayState.Score.Replay.HasReceivedAllFrames = true; - break; - case SpectatedUserState.Playing: Schedule(() => OnNewPlayingUserState(userId, newState)); startGameplay(userId); break; + + case SpectatedUserState.Passed: + case SpectatedUserState.Failed: + case SpectatedUserState.Quit: + endGameplay(userId, newState); + break; } } - private void onUserStateRemoved(int userId, SpectatorState state) - { - if (!userMap.ContainsKey(userId)) - return; - - if (!gameplayStates.TryGetValue(userId, out var gameplayState)) - return; - - gameplayState.Score.Replay.HasReceivedAllFrames = true; - - gameplayStates.Remove(userId); - Schedule(() => EndGameplay(userId, state)); - } - private void startGameplay(int userId) { Debug.Assert(userMap.ContainsKey(userId)); @@ -196,6 +182,20 @@ namespace osu.Game.Screens.Spectate Schedule(() => StartGameplay(userId, gameplayState)); } + private void endGameplay(int userId, SpectatorState state) + { + if (!userMap.ContainsKey(userId)) + return; + + if (!gameplayStates.TryGetValue(userId, out var gameplayState)) + return; + + gameplayState.Score.Replay.HasReceivedAllFrames = true; + + gameplayStates.Remove(userId); + Schedule(() => EndGameplay(userId, state)); + } + /// /// Invoked when a spectated user's state has changed to a new state indicating the player is currently playing. /// @@ -226,7 +226,7 @@ namespace osu.Game.Screens.Spectate if (!userStates.TryGetValue(userId, out var state)) return; - onUserStateRemoved(userId, state); + endGameplay(userId, state); users.Remove(userId); userMap.Remove(userId); From b564c34dbc29a9c821da648177e9051a7a5bb431 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:35:30 +0900 Subject: [PATCH 315/532] Don't process master clock (is a noop) --- .../OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs index 4e785ad3b1..764ab60d6b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs @@ -83,8 +83,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate ElapsedFrameTime = 0; FramesPerSecond = 0; - masterClock.ProcessFrame(); - if (IsRunning) { double elapsedSource = masterClock.ElapsedFrameTime; From 2f5be6efcaecc48c7e6c87cb8a73c19ce2f224f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:37:14 +0900 Subject: [PATCH 316/532] Tidy up `ProcessFrame` and privatise const --- .../Multiplayer/Spectate/SpectatorPlayerClock.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs index 764ab60d6b..be9f7f2bf0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// The catch up rate. /// - public const double CATCHUP_RATE = 2; + private const double catchup_rate = 2; private readonly GameplayClockContainer masterClock; @@ -68,7 +68,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { } - public double Rate => IsCatchingUp ? CATCHUP_RATE : 1; + public double Rate => IsCatchingUp ? catchup_rate : 1; double IAdjustableClock.Rate { @@ -76,13 +76,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate set => throw new NotSupportedException(); } - double IClock.Rate => Rate; - public void ProcessFrame() { - ElapsedFrameTime = 0; - FramesPerSecond = 0; - if (IsRunning) { double elapsedSource = masterClock.ElapsedFrameTime; @@ -92,6 +87,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate ElapsedFrameTime = elapsed; FramesPerSecond = masterClock.FramesPerSecond; } + else + { + ElapsedFrameTime = 0; + FramesPerSecond = 0; + } } public double ElapsedFrameTime { get; private set; } From d05d8aeb2289e6395f800ea724491081aa25a9d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:38:25 +0900 Subject: [PATCH 317/532] Simplify interface implementations --- .../Multiplayer/Spectate/SpectatorPlayerClock.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs index be9f7f2bf0..6f2d1701c9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs @@ -68,12 +68,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { } - public double Rate => IsCatchingUp ? catchup_rate : 1; - - double IAdjustableClock.Rate + public double Rate { - get => Rate; - set => throw new NotSupportedException(); + get => IsCatchingUp ? catchup_rate : 1; + set => throw new NotImplementedException(); } public void ProcessFrame() From d33d705684512d2c3f3b58fcb2d0b14662f4ee5b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:40:47 +0900 Subject: [PATCH 318/532] Make `WaitingOnFrames` non-bindable --- osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs | 6 +++--- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs | 6 +----- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs | 2 +- .../OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs | 3 +-- .../OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs | 4 ++-- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index 5761a89ae8..6015c92663 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -146,12 +146,12 @@ namespace osu.Game.Tests.OnlinePlay } private void setWaiting(Func playerClock, bool waiting) - => AddStep($"set player clock {clocksById[playerClock()]} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = waiting); + => AddStep($"set player clock {clocksById[playerClock()]} waiting = {waiting}", () => playerClock().WaitingOnFrames = waiting); private void setAllWaiting(bool waiting) => AddStep($"set all player clocks waiting = {waiting}", () => { - player1.WaitingOnFrames.Value = waiting; - player2.WaitingOnFrames.Value = waiting; + player1.WaitingOnFrames = waiting; + player2.WaitingOnFrames = waiting; }); private void setMasterTime(double time) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 57af929cb7..d351d121c6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -14,7 +13,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public class MultiSpectatorPlayer : SpectatorPlayer { - private readonly Bindable waitingOnFrames = new Bindable(true); private readonly SpectatorPlayerClock spectatorPlayerClock; /// @@ -31,8 +29,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [BackgroundDependencyLoader] private void load() { - spectatorPlayerClock.WaitingOnFrames.BindTo(waitingOnFrames); - HUDOverlay.PlayerSettingsOverlay.Expire(); HUDOverlay.HoldToQuit.Expire(); } @@ -53,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate base.UpdateAfterChildren(); // This is required because the frame stable clock is set to WaitingOnFrames = false for one frame. - waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || Score.Replay.Frames.Count == 0; + spectatorPlayerClock.WaitingOnFrames = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || Score.Replay.Frames.Count == 0; } protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index b285d3d7c2..8130620312 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -178,7 +178,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } private bool isCandidateAudioSource(SpectatorPlayerClock? clock) - => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value; + => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames; private void onReadyToStart() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs index 6f2d1701c9..7801f22437 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Bindables; using osu.Framework.Timing; using osu.Game.Screens.Play; @@ -25,7 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// Whether this clock is waiting on frames to continue playback. /// - public Bindable WaitingOnFrames { get; } = new Bindable(true); + public bool WaitingOnFrames { get; set; } = true; /// /// Whether this clock is behind the master clock and running at a higher rate to catch up to it. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs index 119e82a682..dcc034cba6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate if (playerClocks.Count == 0) return false; - int readyCount = playerClocks.Count(s => !s.WaitingOnFrames.Value); + int readyCount = playerClocks.Count(s => !s.WaitingOnFrames); if (readyCount == playerClocks.Count) return performStart(); @@ -158,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } // Make sure the player clock is running if it can. - clock.IsRunning = !clock.WaitingOnFrames.Value; + clock.IsRunning = !clock.WaitingOnFrames; if (clock.IsCatchingUp) { From 683d49c6083fbb480fcd9ed12fd48786aed16704 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:51:54 +0900 Subject: [PATCH 319/532] Move `MasterClockState` handling in to `SpectatorSyncManager` --- .../Spectate/MultiSpectatorScreen.cs | 25 ----------------- .../Spectate/SpectatorSyncManager.cs | 27 +++++++++++++++---- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 8130620312..32ba73e904 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -4,11 +4,9 @@ using System; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; @@ -52,7 +50,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private PlayerGrid grid = null!; private MultiSpectatorLeaderboard leaderboard = null!; private PlayerArea? currentAudioSource; - private bool canStartMasterClock; private readonly Room room; private readonly MultiplayerRoomUser[] users; @@ -159,7 +156,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate masterClockContainer.Reset(); syncManager.ReadyToStart += onReadyToStart; - syncManager.MasterState.BindValueChanged(onMasterStateChanged, true); } protected override void Update() @@ -192,27 +188,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate masterClockContainer.StartTime = startTime; masterClockContainer.Reset(true); - - // Although the clock has been started, this flag is set to allow for later synchronisation state changes to also be able to start it. - canStartMasterClock = true; - } - - private void onMasterStateChanged(ValueChangedEvent state) - { - Logger.Log($"{nameof(MultiSpectatorScreen)}'s master clock become {state.NewValue}"); - - switch (state.NewValue) - { - case MasterClockState.Synchronised: - if (canStartMasterClock) - masterClockContainer.Start(); - - break; - - case MasterClockState.TooFarAhead: - masterClockContainer.Stop(); - break; - } } protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs index dcc034cba6..57c7c18db9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Logging; using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate @@ -35,11 +36,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public event Action? ReadyToStart; - /// - /// The catch-up state of the master clock. - /// - public IBindable MasterState => masterState; - public double CurrentMasterTime => masterClock.CurrentTime; /// @@ -55,11 +51,32 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly Bindable masterState = new Bindable(); private bool hasStarted; + private double? firstStartAttemptTime; public SpectatorSyncManager(GameplayClockContainer master) { masterClock = master; + + masterState.BindValueChanged(onMasterStateChanged); + } + + private void onMasterStateChanged(ValueChangedEvent state) + { + Logger.Log($"{nameof(SpectatorSyncManager)}'s master clock become {state.NewValue}"); + + switch (state.NewValue) + { + case MasterClockState.Synchronised: + if (hasStarted) + masterClock.Start(); + + break; + + case MasterClockState.TooFarAhead: + masterClock.Stop(); + break; + } } /// From 6c50f618a3989959b3ceab17962802abccf538d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:53:45 +0900 Subject: [PATCH 320/532] Don't use bindable flow for `masterState` --- .../Spectate/SpectatorSyncManager.cs | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs index 57c7c18db9..af16d4e0ab 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Screens.Play; @@ -48,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// private readonly List playerClocks = new List(); - private readonly Bindable masterState = new Bindable(); + private MasterClockState masterState = MasterClockState.Synchronised; private bool hasStarted; @@ -57,26 +56,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public SpectatorSyncManager(GameplayClockContainer master) { masterClock = master; - - masterState.BindValueChanged(onMasterStateChanged); - } - - private void onMasterStateChanged(ValueChangedEvent state) - { - Logger.Log($"{nameof(SpectatorSyncManager)}'s master clock become {state.NewValue}"); - - switch (state.NewValue) - { - case MasterClockState.Synchronised: - if (hasStarted) - masterClock.Start(); - - break; - - case MasterClockState.TooFarAhead: - masterClock.Stop(); - break; - } } /// @@ -197,8 +176,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// private void updateMasterState() { - bool anyInSync = playerClocks.Any(s => !s.IsCatchingUp); - masterState.Value = anyInSync ? MasterClockState.Synchronised : MasterClockState.TooFarAhead; + MasterClockState newState = playerClocks.Any(s => !s.IsCatchingUp) ? MasterClockState.Synchronised : MasterClockState.TooFarAhead; + + if (masterState == newState) + return; + + masterState = newState; + Logger.Log($"{nameof(SpectatorSyncManager)}'s master clock become {masterState}"); + + switch (masterState) + { + case MasterClockState.Synchronised: + if (hasStarted) + masterClock.Start(); + + break; + + case MasterClockState.TooFarAhead: + masterClock.Stop(); + break; + } } } } From 871365bbb01b8301333da9cd7d0b04d61f454fa4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 15:56:36 +0900 Subject: [PATCH 321/532] Inline `ReadyToStart` action binding for added safety --- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 9 +++++---- .../Multiplayer/Spectate/SpectatorSyncManager.cs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 32ba73e904..3b26fd9962 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -78,7 +78,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate InternalChildren = new[] { - (Drawable)(syncManager = new SpectatorSyncManager(masterClockContainer)), + (Drawable)(syncManager = new SpectatorSyncManager(masterClockContainer) + { + ReadyToStart = performInitialSeek, + }), masterClockContainer.WithChild(new GridContainer { RelativeSizeAxes = Axes.Both, @@ -154,8 +157,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate base.LoadComplete(); masterClockContainer.Reset(); - - syncManager.ReadyToStart += onReadyToStart; } protected override void Update() @@ -176,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private bool isCandidateAudioSource(SpectatorPlayerClock? clock) => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames; - private void onReadyToStart() + private void performInitialSeek() { // Seek the master clock to the gameplay time. // This is chosen as the first available frame in the players' replays, which matches the seek by each individual SpectatorPlayer. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs index af16d4e0ab..8d087aa25c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// An event which is invoked when gameplay is ready to start. /// - public event Action? ReadyToStart; + public Action? ReadyToStart; public double CurrentMasterTime => masterClock.CurrentTime; From 7c1fc4814e02ff20a9709fa7e18160907d0104dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 16:00:20 +0900 Subject: [PATCH 322/532] Remove unused `CreateMasterGameplayClockContainer` method --- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 3b26fd9962..4cab377239 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -74,7 +73,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate FillFlowContainer leaderboardFlow; Container scoreDisplayContainer; - masterClockContainer = CreateMasterGameplayClockContainer(Beatmap.Value); + masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0); InternalChildren = new[] { @@ -230,7 +229,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return base.OnBackButton(); } - - protected virtual MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap) => new MasterGameplayClockContainer(beatmap, 0); } } From edd50dc05bd787800609dc1d44f210f3965c2d1a Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 24 Aug 2022 03:07:03 -0400 Subject: [PATCH 323/532] Add profile url context menu to user container --- .../Profile/Header/TopHeaderContainer.cs | 144 +++++++++--------- 1 file changed, 75 insertions(+), 69 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 7e079c8341..67a6df3228 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -70,85 +71,90 @@ namespace osu.Game.Overlays.Profile.Header Masking = true, CornerRadius = avatar_size * 0.25f, }, - new Container + new OsuContextMenuContainer { RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, - Padding = new MarginPadding { Left = 10 }, - Children = new Drawable[] + Child = new Container { - new FillFlowContainer + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Padding = new MarginPadding { Left = 10 }, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] + new FillFlowContainer { - new FillFlowContainer + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new Drawable[] + new FillFlowContainer { - usernameText = new OsuSpriteText + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] { - Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular) - }, - openUserExternally = new ExternalLinkButton - { - Margin = new MarginPadding { Left = 5 }, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - } - }, - titleText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular) - }, - } - }, - new FillFlowContainer - { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - supporterTag = new SupporterIcon - { - Height = 20, - Margin = new MarginPadding { Top = 5 } - }, - new Box - { - RelativeSizeAxes = Axes.X, - Height = 1.5f, - Margin = new MarginPadding { Top = 10 }, - Colour = colourProvider.Light1, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 5 }, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - userFlag = new UpdateableFlag - { - Size = new Vector2(28, 20), - ShowPlaceholderOnUnknown = false, - }, - userCountryText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular), - Margin = new MarginPadding { Left = 10 }, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Colour = colourProvider.Light1, + usernameText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular) + }, + openUserExternally = new ExternalLinkButton + { + Margin = new MarginPadding { Left = 5 }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, } - } - }, + }, + titleText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular) + }, + } + }, + new FillFlowContainer + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + supporterTag = new SupporterIcon + { + Height = 20, + Margin = new MarginPadding { Top = 5 } + }, + new Box + { + RelativeSizeAxes = Axes.X, + Height = 1.5f, + Margin = new MarginPadding { Top = 10 }, + Colour = colourProvider.Light1, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5 }, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + userFlag = new UpdateableFlag + { + Size = new Vector2(28, 20), + ShowPlaceholderOnUnknown = false, + }, + userCountryText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular), + Margin = new MarginPadding { Left = 10 }, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Colour = colourProvider.Light1, + } + } + }, + } } } } From 7f9246637a6276a70addae935aac728b1d86c833 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 16:08:48 +0900 Subject: [PATCH 324/532] Simplify `MultiSpectatorScreen` hierarchy construction --- .../Spectate/MultiSpectatorScreen.cs | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 4cab377239..cb797d7aff 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -73,53 +73,54 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate FillFlowContainer leaderboardFlow; Container scoreDisplayContainer; - masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0); - - InternalChildren = new[] + InternalChildren = new Drawable[] { - (Drawable)(syncManager = new SpectatorSyncManager(masterClockContainer) + masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0) { - ReadyToStart = performInitialSeek, - }), - masterClockContainer.WithChild(new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, - Content = new[] + Child = new GridContainer { - new Drawable[] + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + Content = new[] { - scoreDisplayContainer = new Container + new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }, - }, - new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, - Content = new[] + scoreDisplayContainer = new Container { - new Drawable[] + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }, + }, + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + Content = new[] { - leaderboardFlow = new FillFlowContainer + new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(5) - }, - grid = new PlayerGrid { RelativeSizeAxes = Axes.Both } + leaderboardFlow = new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5) + }, + grid = new PlayerGrid { RelativeSizeAxes = Axes.Both } + } } } } } } - }) + }, + syncManager = new SpectatorSyncManager(masterClockContainer) + { + ReadyToStart = performInitialSeek, + } }; for (int i = 0; i < Users.Count; i++) From b24513038cd66941e26b304dd8667957850243e8 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 24 Aug 2022 02:54:24 -0400 Subject: [PATCH 325/532] Add popupdialog button to copy url --- osu.Game/Online/Chat/ExternalLinkOpener.cs | 2 +- osu.Game/Overlays/Chat/ExternalLinkDialog.cs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index e557b9933e..4141a6fb60 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -32,7 +32,7 @@ namespace osu.Game.Online.Chat public void OpenUrlExternally(string url, bool bypassWarning = false) { if (!bypassWarning && externalLinkWarning.Value) - dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url))); + dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => host.GetClipboard().SetText(url))); else host.OpenUrlExternally(url); } diff --git a/osu.Game/Overlays/Chat/ExternalLinkDialog.cs b/osu.Game/Overlays/Chat/ExternalLinkDialog.cs index f0d39346e0..09650a13bd 100644 --- a/osu.Game/Overlays/Chat/ExternalLinkDialog.cs +++ b/osu.Game/Overlays/Chat/ExternalLinkDialog.cs @@ -11,7 +11,7 @@ namespace osu.Game.Overlays.Chat { public class ExternalLinkDialog : PopupDialog { - public ExternalLinkDialog(string url, Action openExternalLinkAction) + public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) { HeaderText = "Just checking..."; BodyText = $"You are about to leave osu! and open the following link in a web browser:\n\n{url}"; @@ -25,6 +25,11 @@ namespace osu.Game.Overlays.Chat Text = @"Yes. Go for it.", Action = openExternalLinkAction }, + new PopupDialogOkButton + { + Text = @"No! Copy the URL instead!", + Action = copyExternalLinkAction + }, new PopupDialogCancelButton { Text = @"No! Abort mission!" From 8f4a2b4936636031bd394bd54a0ddd9b3129c86f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Aug 2022 01:37:43 +0300 Subject: [PATCH 326/532] Separate passed/failed states from calling `EndGameplay` --- .../Spectate/MultiSpectatorScreen.cs | 13 +------ osu.Game/Screens/Play/SoloSpectator.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 34 ++++++++++++------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 3d04ae8f3c..9269433ac5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -219,19 +219,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) => instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score); - protected override void EndGameplay(int userId, SpectatorState state) + protected override void QuitGameplay(int userId) { - // Allowed passed/failed users to complete their remaining replay frames. - // The failed state isn't really possible in multiplayer (yet?) but is added here just for safety in case it starts being used. - if (state.State == SpectatedUserState.Passed || state.State == SpectatedUserState.Failed) - return; - - // we could also potentially receive EndGameplay with "Playing" state, at which point we can only early-return and hope it's a passing player. - // todo: this shouldn't exist, but it's here as a hotfix for an issue with multi-spectator screen not proceeding to results screen. - // see: https://github.com/ppy/osu/issues/19593 - if (state.State == SpectatedUserState.Playing) - return; - RemoveUser(userId); var instance = instances.Single(i => i.UserId == userId); diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index 7eaba40640..9ef05c3a05 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -182,7 +182,7 @@ namespace osu.Game.Screens.Play scheduleStart(spectatorGameplayState); } - protected override void EndGameplay(int userId, SpectatorState state) + protected override void QuitGameplay(int userId) { scheduledStart?.Cancel(); immediateSpectatorGameplayState = null; diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 8218ddf641..7081db4793 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -142,9 +142,11 @@ namespace osu.Game.Screens.Spectate break; case SpectatedUserState.Passed: - case SpectatedUserState.Failed: + markReceivedAllFrames(userId); + break; + case SpectatedUserState.Quit: - endGameplay(userId, newState); + quitGameplay(userId); break; } } @@ -182,18 +184,27 @@ namespace osu.Game.Screens.Spectate Schedule(() => StartGameplay(userId, gameplayState)); } - private void endGameplay(int userId, SpectatorState state) + /// + /// Marks an existing gameplay session as received all frames. + /// + private void markReceivedAllFrames(int userId) + { + if (gameplayStates.TryGetValue(userId, out var gameplayState)) + gameplayState.Score.Replay.HasReceivedAllFrames = true; + } + + private void quitGameplay(int userId) { if (!userMap.ContainsKey(userId)) return; - if (!gameplayStates.TryGetValue(userId, out var gameplayState)) + if (!gameplayStates.ContainsKey(userId)) return; - gameplayState.Score.Replay.HasReceivedAllFrames = true; + markReceivedAllFrames(userId); gameplayStates.Remove(userId); - Schedule(() => EndGameplay(userId, state)); + Schedule(() => QuitGameplay(userId)); } /// @@ -211,11 +222,10 @@ namespace osu.Game.Screens.Spectate protected abstract void StartGameplay(int userId, [NotNull] SpectatorGameplayState spectatorGameplayState); /// - /// Ends gameplay for a user. + /// Quits gameplay for a user. /// - /// The user to end gameplay for. - /// The final user state. - protected abstract void EndGameplay(int userId, SpectatorState state); + /// The user to quit gameplay for. + protected abstract void QuitGameplay(int userId); /// /// Stops spectating a user. @@ -223,10 +233,10 @@ namespace osu.Game.Screens.Spectate /// The user to stop spectating. protected void RemoveUser(int userId) { - if (!userStates.TryGetValue(userId, out var state)) + if (!userStates.ContainsKey(userId)) return; - endGameplay(userId, state); + quitGameplay(userId); users.Remove(userId); userMap.Remove(userId); From adea29c10604fb50a91d680e751f744c84cfe559 Mon Sep 17 00:00:00 2001 From: Khang Date: Wed, 24 Aug 2022 03:37:33 -0400 Subject: [PATCH 327/532] Fix test failures --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 4 +--- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 7bd32eb5bd..9fc1eb7650 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -931,16 +931,14 @@ namespace osu.Game.Tests.Beatmaps.Formats var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo; Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(1)); - Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(3)); + Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(2)); Assert.That(controlPoints.TimingPointAt(1000).BeatLength, Is.EqualTo(500)); - Assert.That(controlPoints.DifficultyPointAt(1000).SliderVelocity, Is.EqualTo(1)); Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1)); Assert.That(controlPoints.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(1)); #pragma warning disable 618 - Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(1000)).GenerateTicks, Is.True); Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(2000)).GenerateTicks, Is.False); Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(3000)).GenerateTicks, Is.True); #pragma warning restore 618 diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index c3fd16e86f..3d65ab8e0f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -188,9 +188,8 @@ namespace osu.Game.Beatmaps.Formats } public override bool IsRedundant(ControlPoint? existing) - => existing is LegacyDifficultyControlPoint existingLegacyDifficulty - && base.IsRedundant(existing) - && GenerateTicks == existingLegacyDifficulty.GenerateTicks; + => base.IsRedundant(existing) + && GenerateTicks == ((existing as LegacyDifficultyControlPoint)?.GenerateTicks ?? true); public override void CopyFrom(ControlPoint other) { From ec5fd7ac1dc0bf84328a64a3b963bf1157ee6685 Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 24 Aug 2022 03:42:16 -0400 Subject: [PATCH 328/532] Remove possible 'System.NullReferenceException' --- osu.Game/Online/Chat/ExternalLinkOpener.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 4141a6fb60..4c5df9c917 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -32,7 +32,7 @@ namespace osu.Game.Online.Chat public void OpenUrlExternally(string url, bool bypassWarning = false) { if (!bypassWarning && externalLinkWarning.Value) - dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => host.GetClipboard().SetText(url))); + dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => host.GetClipboard()?.SetText(url))); else host.OpenUrlExternally(url); } From e378c5b866119b48a2b4ff96ed383706935ed60e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 24 Aug 2022 10:50:40 +0300 Subject: [PATCH 329/532] Remove no longer necessary switch case --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 7081db4793..259ac0160d 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -118,11 +118,6 @@ namespace osu.Game.Screens.Spectate foreach ((int userId, SpectatorState state) in e.NewItems.AsNonNull()) onUserStateChanged(userId, state); break; - - case NotifyDictionaryChangedAction.Remove: - foreach ((int userId, SpectatorState state) in e.OldItems.AsNonNull()) - onUserStateChanged(userId, state); - break; } } From af56cd0126b1cc4b5dd5254b02e4ae64e44d77ea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 16:52:36 +0900 Subject: [PATCH 330/532] Fix merge breakage --- .../Spectate/CatchUpSpectatorPlayerClock.cs | 98 ------------------- .../Spectate/MultiSpectatorScreen.cs | 3 +- 2 files changed, 1 insertion(+), 100 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs deleted file mode 100644 index 5625a79afa..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Bindables; -using osu.Framework.Timing; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate -{ - /// - /// A which catches up using rate adjustment. - /// - public class CatchUpSpectatorPlayerClock : ISpectatorPlayerClock - { - /// - /// The catch up rate. - /// - public const double CATCHUP_RATE = 2; - - public readonly IFrameBasedClock Source; - - public double CurrentTime { get; private set; } - - public bool IsRunning { get; private set; } - - public CatchUpSpectatorPlayerClock(IFrameBasedClock source) - { - Source = source; - } - - public void Reset() => CurrentTime = 0; - - public void Start() => IsRunning = true; - - public void Stop() => IsRunning = false; - - void IAdjustableClock.Start() - { - // Our running state should only be managed by an ISyncManager, ignore calls from external sources. - } - - void IAdjustableClock.Stop() - { - // Our running state should only be managed by an ISyncManager, ignore calls from external sources. - } - - public bool Seek(double position) - { - CurrentTime = position; - return true; - } - - public void ResetSpeedAdjustments() - { - } - - public double Rate => IsCatchingUp ? CATCHUP_RATE : 1; - - double IAdjustableClock.Rate - { - get => Rate; - set => throw new NotSupportedException(); - } - - double IClock.Rate => Rate; - - public void ProcessFrame() - { - ElapsedFrameTime = 0; - FramesPerSecond = 0; - - Source.ProcessFrame(); - - if (IsRunning) - { - // When in catch-up mode, the source is usually not running. - // In such a case, its elapsed time may be zero, which would cause catch-up to get stuck. - // To avoid this, use a constant 16ms elapsed time for now. Probably not too correct, but this whole logic isn't too correct anyway. - double elapsedSource = Source.IsRunning ? Source.ElapsedFrameTime : 16; - double elapsed = elapsedSource * Rate; - - CurrentTime += elapsed; - ElapsedFrameTime = elapsed; - FramesPerSecond = Source.FramesPerSecond; - } - } - - public double ElapsedFrameTime { get; private set; } - - public double FramesPerSecond { get; private set; } - - public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime }; - - public Bindable WaitingOnFrames { get; } = new Bindable(true); - - public bool IsCatchingUp { get; set; } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index cb797d7aff..aa002a7da2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -187,8 +187,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate .DefaultIfEmpty(0) .Min(); - masterClockContainer.StartTime = startTime; - masterClockContainer.Reset(true); + masterClockContainer.Reset(startTime, true); } protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) From 9ee26c575d1d68f7c6fc31911653212c220fa8cc Mon Sep 17 00:00:00 2001 From: Andrew Hong Date: Wed, 24 Aug 2022 04:04:44 -0400 Subject: [PATCH 331/532] Made button blue --- osu.Game/Overlays/Chat/ExternalLinkDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ExternalLinkDialog.cs b/osu.Game/Overlays/Chat/ExternalLinkDialog.cs index 09650a13bd..b5102de1c5 100644 --- a/osu.Game/Overlays/Chat/ExternalLinkDialog.cs +++ b/osu.Game/Overlays/Chat/ExternalLinkDialog.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Chat Text = @"Yes. Go for it.", Action = openExternalLinkAction }, - new PopupDialogOkButton + new PopupDialogCancelButton { Text = @"No! Copy the URL instead!", Action = copyExternalLinkAction From 5f01f461b375c33e2b90c9813314e0376642d3fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 16:33:07 +0900 Subject: [PATCH 332/532] Ensure elapsed time is always non-zero when advancing `SpectatorPlayerClock` --- .../Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs | 8 +++++++- .../Multiplayer/Spectate/SpectatorPlayerClock.cs | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index bab613bed7..dce996696b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -357,12 +357,18 @@ namespace osu.Game.Tests.Visual.Multiplayer /// /// Tests spectating with a beatmap that has a high value. + /// + /// This test is not intended not to check the correct initial time value, but only to guard against + /// gameplay potentially getting stuck in a stopped state due to lead in time being present. /// [Test] public void TestAudioLeadIn() => testLeadIn(b => b.BeatmapInfo.AudioLeadIn = 2000); /// /// Tests spectating with a beatmap that has a storyboard element with a negative start time (i.e. intro storyboard element). + /// + /// This test is not intended not to check the correct initial time value, but only to guard against + /// gameplay potentially getting stuck in a stopped state due to lead in time being present. /// [Test] public void TestIntroStoryboardElement() => testLeadIn(b => @@ -384,7 +390,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for player load", () => spectatorScreen.AllPlayersLoaded); - AddWaitStep("wait for progression", 3); + AddUntilStep($"wait for clock running", () => getInstance(PLAYER_1_ID).SpectatorPlayerClock.IsRunning); assertNotCatchingUp(PLAYER_1_ID); assertRunning(PLAYER_1_ID); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs index 7801f22437..62731c6903 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs @@ -77,7 +77,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { if (IsRunning) { - double elapsedSource = masterClock.ElapsedFrameTime; + // When in catch-up mode, the source is usually not running. + // In such a case, its elapsed time may be zero, which would cause catch-up to get stuck. + // To avoid this, use a constant 16ms elapsed time for now. Probably not too correct, but this whole logic isn't too correct anyway. + double elapsedSource = masterClock.ElapsedFrameTime != 0 ? masterClock.ElapsedFrameTime : 16; double elapsed = elapsedSource * Rate; CurrentTime += elapsed; From 27b57947e4822e1e73757d434b8fa8640327f2eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 16:43:26 +0900 Subject: [PATCH 333/532] Rename `PlayerArea.GameplayClock` to `SpectatorPlayerClock` for clarity --- .../Multiplayer/TestSceneMultiSpectatorScreen.cs | 6 +++--- .../Multiplayer/Spectate/MultiSpectatorScreen.cs | 10 +++++----- .../OnlinePlay/Multiplayer/Spectate/PlayerArea.cs | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index dce996696b..54e289055b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -473,13 +473,13 @@ namespace osu.Game.Tests.Visual.Multiplayer => AddAssert($"{userId} {(muted ? "is" : "is not")} muted", () => getInstance(userId).Mute == muted); private void assertRunning(int userId) - => AddAssert($"{userId} clock running", () => getInstance(userId).GameplayClock.IsRunning); + => AddAssert($"{userId} clock running", () => getInstance(userId).SpectatorPlayerClock.IsRunning); private void assertNotCatchingUp(int userId) - => AddAssert($"{userId} in sync", () => !getInstance(userId).GameplayClock.IsCatchingUp); + => AddAssert($"{userId} in sync", () => !getInstance(userId).SpectatorPlayerClock.IsCatchingUp); private void waitForCatchup(int userId) - => AddUntilStep($"{userId} not catching up", () => !getInstance(userId).GameplayClock.IsCatchingUp); + => AddUntilStep($"{userId} not catching up", () => !getInstance(userId).SpectatorPlayerClock.IsCatchingUp); private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType().Single(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index aa002a7da2..a42aa4ba93 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -132,7 +132,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate }, _ => { foreach (var instance in instances) - leaderboard.AddClock(instance.UserId, instance.GameplayClock); + leaderboard.AddClock(instance.UserId, instance.SpectatorPlayerClock); leaderboardFlow.Insert(0, leaderboard); @@ -163,10 +163,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { base.Update(); - if (!isCandidateAudioSource(currentAudioSource?.GameplayClock)) + if (!isCandidateAudioSource(currentAudioSource?.SpectatorPlayerClock)) { - currentAudioSource = instances.Where(i => isCandidateAudioSource(i.GameplayClock)) - .OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.CurrentMasterTime)) + currentAudioSource = instances.Where(i => isCandidateAudioSource(i.SpectatorPlayerClock)) + .OrderBy(i => Math.Abs(i.SpectatorPlayerClock.CurrentTime - syncManager.CurrentMasterTime)) .FirstOrDefault(); foreach (var instance in instances) @@ -215,7 +215,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate var instance = instances.Single(i => i.UserId == userId); instance.FadeColour(colours.Gray4, 400, Easing.OutQuint); - syncManager.RemoveManagedClock(instance.GameplayClock); + syncManager.RemoveManagedClock(instance.SpectatorPlayerClock); } public override bool OnBackButton() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index a1fbdc10de..36f6631ebf 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -38,9 +38,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public readonly int UserId; /// - /// The used to control the gameplay running state of a loaded . + /// The used to control the gameplay running state of a loaded . /// - public readonly SpectatorPlayerClock GameplayClock; + public readonly SpectatorPlayerClock SpectatorPlayerClock; /// /// The currently-loaded score. @@ -58,7 +58,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public PlayerArea(int userId, SpectatorPlayerClock clock) { UserId = userId; - GameplayClock = clock; + SpectatorPlayerClock = clock; RelativeSizeAxes = Axes.Both; Masking = true; @@ -95,7 +95,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate stack.Push(new MultiSpectatorPlayerLoader(Score, () => { - var player = new MultiSpectatorPlayer(Score, GameplayClock); + var player = new MultiSpectatorPlayer(Score, SpectatorPlayerClock); player.OnGameplayStarted += () => OnGameplayStarted?.Invoke(); return player; })); From ddccf4defe293ebd44a59999a27d3bb4e6d9473b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 17:17:59 +0900 Subject: [PATCH 334/532] Remove dollar sign --- .../Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 54e289055b..3226eb992d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -390,7 +390,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for player load", () => spectatorScreen.AllPlayersLoaded); - AddUntilStep($"wait for clock running", () => getInstance(PLAYER_1_ID).SpectatorPlayerClock.IsRunning); + AddUntilStep("wait for clock running", () => getInstance(PLAYER_1_ID).SpectatorPlayerClock.IsRunning); assertNotCatchingUp(PLAYER_1_ID); assertRunning(PLAYER_1_ID); From f70af779a4dd9e3215ede924fce791d31461c5f2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Aug 2022 17:42:41 +0900 Subject: [PATCH 335/532] Add maximum statistics to ScoreInfo/SoloScoreInfo --- .../API/Requests/Responses/SoloScoreInfo.cs | 5 +++++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 7 ++++--- osu.Game/Scoring/ScoreInfo.cs | 21 +++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index e2e5ea4239..16aa800cb0 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -74,6 +74,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("statistics")] public Dictionary Statistics { get; set; } = new Dictionary(); + [JsonProperty("maximum_statistics")] + public Dictionary MaximumStatistics { get; set; } = new Dictionary(); + #region osu-web API additions (not stored to database). [JsonProperty("id")] @@ -153,6 +156,7 @@ namespace osu.Game.Online.API.Requests.Responses MaxCombo = MaxCombo, Rank = Rank, Statistics = Statistics, + MaximumStatistics = MaximumStatistics, Date = EndedAt, Hash = HasReplay ? "online" : string.Empty, // TODO: temporary? Mods = mods, @@ -174,6 +178,7 @@ namespace osu.Game.Online.API.Requests.Responses Passed = score.Passed, Mods = score.APIMods, Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), + MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), }; public long OnlineID => ID ?? -1; diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 1deac9f08a..9e7a5e3657 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -405,8 +405,6 @@ namespace osu.Game.Rulesets.Scoring return ScoreRank.D; } - public int GetStatistic(HitResult result) => scoreResultCounts.GetValueOrDefault(result); - /// /// Resets this ScoreProcessor to a default state. /// @@ -449,7 +447,10 @@ namespace osu.Game.Rulesets.Scoring score.HitEvents = hitEvents; foreach (var result in HitResultExtensions.ALL_TYPES) - score.Statistics[result] = GetStatistic(result); + score.Statistics[result] = scoreResultCounts.GetValueOrDefault(result); + + foreach (var result in HitResultExtensions.ALL_TYPES) + score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result); // Populate total score after everything else. score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score)); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index d32d611a27..99e0726da7 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -63,6 +63,9 @@ namespace osu.Game.Scoring [MapTo("Statistics")] public string StatisticsJson { get; set; } = string.Empty; + [MapTo("MaximumStatistics")] + public string MaximumStatisticsJson { get; set; } = string.Empty; + public ScoreInfo(BeatmapInfo? beatmap = null, RulesetInfo? ruleset = null, RealmUser? realmUser = null) { Ruleset = ruleset ?? new RulesetInfo(); @@ -181,6 +184,24 @@ namespace osu.Game.Scoring set => statistics = value; } + private Dictionary? maximumStatistics; + + [Ignored] + public Dictionary MaximumStatistics + { + get + { + if (maximumStatistics != null) + return maximumStatistics; + + if (!string.IsNullOrEmpty(MaximumStatisticsJson)) + maximumStatistics = JsonConvert.DeserializeObject>(MaximumStatisticsJson); + + return maximumStatistics ??= new Dictionary(); + } + set => maximumStatistics = value; + } + private Mod[]? mods; [Ignored] From d947a6cb5947c1a3bf1a1d81fd7701266945ffae Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Aug 2022 19:45:19 +0900 Subject: [PATCH 336/532] Add Realm migration --- osu.Game/Database/RealmAccess.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 0f2e724567..e23fc912df 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -69,8 +69,9 @@ namespace osu.Game.Database /// 21 2022-07-27 Migrate collections to realm (BeatmapCollection). /// 22 2022-07-31 Added ModPreset. /// 23 2022-08-01 Added LastLocalUpdate to BeatmapInfo. + /// 24 2022-08-22 Added MaximumStatistics to ScoreInfo. /// - private const int schema_version = 23; + private const int schema_version = 24; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. From cc648a90bc547fdb9c368937b9a9cd618a83191b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Aug 2022 21:31:10 +0900 Subject: [PATCH 337/532] Actually save maximum statistics --- osu.Game/Scoring/ScoreImporter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 0902f1636b..45f827354e 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -73,6 +73,9 @@ namespace osu.Game.Scoring if (string.IsNullOrEmpty(model.StatisticsJson)) model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics); + + if (string.IsNullOrEmpty(model.MaximumStatisticsJson)) + model.MaximumStatisticsJson = JsonConvert.SerializeObject(model.MaximumStatistics); } protected override void PostImport(ScoreInfo model, Realm realm, bool batchImport) From 9f9deef438a557254142acb2cb38ec1a96d503c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 17:37:41 +0900 Subject: [PATCH 338/532] Reword slightly --- osu.Game/Overlays/Chat/ExternalLinkDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ExternalLinkDialog.cs b/osu.Game/Overlays/Chat/ExternalLinkDialog.cs index b5102de1c5..657fd35f8a 100644 --- a/osu.Game/Overlays/Chat/ExternalLinkDialog.cs +++ b/osu.Game/Overlays/Chat/ExternalLinkDialog.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Chat }, new PopupDialogCancelButton { - Text = @"No! Copy the URL instead!", + Text = @"Copy URL to the clipboard instead.", Action = copyExternalLinkAction }, new PopupDialogCancelButton From 6a0d23cf96158cad6007c160713e16e4a5ddeb75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 17:39:22 +0900 Subject: [PATCH 339/532] Nest dialog class and apply NRT --- osu.Game/Online/Chat/ExternalLinkOpener.cs | 43 ++++++++++++++++---- osu.Game/Overlays/Chat/ExternalLinkDialog.cs | 40 ------------------ 2 files changed, 36 insertions(+), 47 deletions(-) delete mode 100644 osu.Game/Overlays/Chat/ExternalLinkDialog.cs diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs index 4c5df9c917..587159179f 100644 --- a/osu.Game/Online/Chat/ExternalLinkOpener.cs +++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs @@ -1,27 +1,27 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; using osu.Game.Configuration; using osu.Game.Overlays; -using osu.Game.Overlays.Chat; +using osu.Game.Overlays.Dialog; namespace osu.Game.Online.Chat { public class ExternalLinkOpener : Component { [Resolved] - private GameHost host { get; set; } + private GameHost host { get; set; } = null!; [Resolved(CanBeNull = true)] - private IDialogOverlay dialogOverlay { get; set; } + private IDialogOverlay? dialogOverlay { get; set; } - private Bindable externalLinkWarning; + private Bindable externalLinkWarning = null!; [BackgroundDependencyLoader(true)] private void load(OsuConfigManager config) @@ -31,10 +31,39 @@ namespace osu.Game.Online.Chat public void OpenUrlExternally(string url, bool bypassWarning = false) { - if (!bypassWarning && externalLinkWarning.Value) + if (!bypassWarning && externalLinkWarning.Value && dialogOverlay != null) dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => host.GetClipboard()?.SetText(url))); else host.OpenUrlExternally(url); } + + public class ExternalLinkDialog : PopupDialog + { + public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) + { + HeaderText = "Just checking..."; + BodyText = $"You are about to leave osu! and open the following link in a web browser:\n\n{url}"; + + Icon = FontAwesome.Solid.ExclamationTriangle; + + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = @"Yes. Go for it.", + Action = openExternalLinkAction + }, + new PopupDialogCancelButton + { + Text = @"Copy URL to the clipboard instead.", + Action = copyExternalLinkAction + }, + new PopupDialogCancelButton + { + Text = @"No! Abort mission!" + }, + }; + } + } } } diff --git a/osu.Game/Overlays/Chat/ExternalLinkDialog.cs b/osu.Game/Overlays/Chat/ExternalLinkDialog.cs deleted file mode 100644 index 657fd35f8a..0000000000 --- a/osu.Game/Overlays/Chat/ExternalLinkDialog.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using System; -using osu.Framework.Graphics.Sprites; -using osu.Game.Overlays.Dialog; - -namespace osu.Game.Overlays.Chat -{ - public class ExternalLinkDialog : PopupDialog - { - public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) - { - HeaderText = "Just checking..."; - BodyText = $"You are about to leave osu! and open the following link in a web browser:\n\n{url}"; - - Icon = FontAwesome.Solid.ExclamationTriangle; - - Buttons = new PopupDialogButton[] - { - new PopupDialogOkButton - { - Text = @"Yes. Go for it.", - Action = openExternalLinkAction - }, - new PopupDialogCancelButton - { - Text = @"Copy URL to the clipboard instead.", - Action = copyExternalLinkAction - }, - new PopupDialogCancelButton - { - Text = @"No! Abort mission!" - }, - }; - } - } -} From 5ec95c92694f47ca376ff6a3e54d4b66fee5410e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Aug 2022 19:46:35 +0900 Subject: [PATCH 340/532] Update ScoreProcessor to make use of MaximumStatistics --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 9e7a5e3657..866f285e4f 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -535,6 +535,9 @@ namespace osu.Game.Rulesets.Scoring { extractScoringValues(scoreInfo.Statistics, out current, out maximum); current.MaxCombo = scoreInfo.MaxCombo; + + if (scoreInfo.MaximumStatistics.Count > 0) + extractScoringValues(scoreInfo.MaximumStatistics, out _, out maximum); } /// From c9ff39f8c33d014462db360d6c19d45653ebf32c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 Aug 2022 21:00:30 +0900 Subject: [PATCH 341/532] Add HitResult.LegacyComboIncrease --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 69 +++++++++++++++++++ osu.Game/Rulesets/Scoring/HitResult.cs | 38 ++++++++-- .../Rulesets/Scoring/JudgementProcessor.cs | 5 ++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 3 +- 4 files changed, 110 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index ff282fff62..63d6e0483b 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -314,6 +315,46 @@ namespace osu.Game.Tests.Rulesets.Scoring }), Is.EqualTo(expectedScore).Within(0.5d)); } +#pragma warning disable CS0618 + [Test] + public void TestLegacyComboIncrease() + { + Assert.That(HitResult.LegacyComboIncrease.IncreasesCombo(), Is.True); + Assert.That(HitResult.LegacyComboIncrease.BreaksCombo(), Is.False); + Assert.That(HitResult.LegacyComboIncrease.AffectsCombo(), Is.True); + Assert.That(HitResult.LegacyComboIncrease.AffectsAccuracy(), Is.False); + Assert.That(HitResult.LegacyComboIncrease.IsBasic(), Is.False); + Assert.That(HitResult.LegacyComboIncrease.IsTick(), Is.False); + Assert.That(HitResult.LegacyComboIncrease.IsBonus(), Is.False); + Assert.That(HitResult.LegacyComboIncrease.IsHit(), Is.True); + Assert.That(HitResult.LegacyComboIncrease.IsScorable(), Is.True); + Assert.That(HitResultExtensions.ALL_TYPES, Does.Not.Contain(HitResult.LegacyComboIncrease)); + + // Cannot be used to apply results. + Assert.Throws(() => scoreProcessor.ApplyBeatmap(new Beatmap + { + HitObjects = { new TestHitObject(HitResult.LegacyComboIncrease) } + })); + + ScoreInfo testScore = new ScoreInfo + { + MaxCombo = 1, + Statistics = new Dictionary + { + { HitResult.Great, 1 } + }, + MaximumStatistics = new Dictionary + { + { HitResult.Great, 1 }, + { HitResult.LegacyComboIncrease, 1 } + } + }; + + double totalScore = new TestScoreProcessor().ComputeFinalScore(ScoringMode.Standardised, testScore); + Assert.That(totalScore, Is.EqualTo(750_000)); // 500K from accuracy (100%), and 250K from combo (50%). + } +#pragma warning restore CS0618 + private class TestRuleset : Ruleset { public override IEnumerable GetModsFor(ModType type) => throw new System.NotImplementedException(); @@ -352,5 +393,33 @@ namespace osu.Game.Tests.Rulesets.Scoring this.maxResult = maxResult; } } + + private class TestScoreProcessor : ScoreProcessor + { + protected override double DefaultAccuracyPortion => 0.5; + protected override double DefaultComboPortion => 0.5; + + public TestScoreProcessor() + : base(new TestRuleset()) + { + } + + // ReSharper disable once MemberHidesStaticFromOuterClass + private class TestRuleset : Ruleset + { + protected override IEnumerable GetValidHitResults() => new[] { HitResult.Great }; + + public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException(); + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new NotImplementedException(); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException(); + + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException(); + + public override string Description => string.Empty; + public override string ShortName => string.Empty; + } + } } } diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index bfa256fc20..e6aba4a70e 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -119,8 +119,20 @@ namespace osu.Game.Rulesets.Scoring [EnumMember(Value = "ignore_hit")] [Order(12)] IgnoreHit, + + /// + /// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not contribute to score. + /// + /// + /// DO NOT USE. + /// + [EnumMember(Value = "legacy_combo_increase")] + [Order(99)] + [Obsolete("Do not use.")] + LegacyComboIncrease = 99 } +#pragma warning disable CS0618 public static class HitResultExtensions { /// @@ -150,6 +162,7 @@ namespace osu.Game.Rulesets.Scoring case HitResult.Perfect: case HitResult.LargeTickHit: case HitResult.LargeTickMiss: + case HitResult.LegacyComboIncrease: return true; default: @@ -161,13 +174,23 @@ namespace osu.Game.Rulesets.Scoring /// Whether a affects the accuracy portion of the score. /// public static bool AffectsAccuracy(this HitResult result) - => IsScorable(result) && !IsBonus(result); + { + if (result == HitResult.LegacyComboIncrease) + return false; + + return IsScorable(result) && !IsBonus(result); + } /// /// Whether a is a non-tick and non-bonus result. /// public static bool IsBasic(this HitResult result) - => IsScorable(result) && !IsTick(result) && !IsBonus(result); + { + if (result == HitResult.LegacyComboIncrease) + return false; + + return IsScorable(result) && !IsTick(result) && !IsBonus(result); + } /// /// Whether a should be counted as a tick. @@ -225,12 +248,18 @@ namespace osu.Game.Rulesets.Scoring /// /// Whether a is scorable. /// - public static bool IsScorable(this HitResult result) => result >= HitResult.Miss && result < HitResult.IgnoreMiss; + public static bool IsScorable(this HitResult result) + { + if (result == HitResult.LegacyComboIncrease) + return true; + + return result >= HitResult.Miss && result < HitResult.IgnoreMiss; + } /// /// An array of all scorable s. /// - public static readonly HitResult[] ALL_TYPES = ((HitResult[])Enum.GetValues(typeof(HitResult))).ToArray(); + public static readonly HitResult[] ALL_TYPES = ((HitResult[])Enum.GetValues(typeof(HitResult))).Except(new[] { HitResult.LegacyComboIncrease }).ToArray(); /// /// Whether a is valid within a given range. @@ -251,4 +280,5 @@ namespace osu.Game.Rulesets.Scoring return result > minResult && result < maxResult; } } +#pragma warning restore CS0618 } diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 12fe0056bb..bc8f2c22f3 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -61,6 +61,11 @@ namespace osu.Game.Rulesets.Scoring /// The to apply. public void ApplyResult(JudgementResult result) { +#pragma warning disable CS0618 + if (result.Type == HitResult.LegacyComboIncrease) + throw new ArgumentException(@$"A {nameof(HitResult.LegacyComboIncrease)} hit result cannot be applied."); +#pragma warning restore CS0618 + JudgedHits++; lastAppliedResult = result; diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 866f285e4f..905fda6d8b 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -593,7 +593,8 @@ namespace osu.Game.Rulesets.Scoring if (result.IsBonus()) current.BonusScore += count * Judgement.ToNumericResult(result); - else + + if (result.AffectsAccuracy()) { // The maximum result of this judgement if it wasn't a miss. // E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT). From e7cbb6c63d784c0365bf22ae6512edc637a80308 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Aug 2022 19:53:16 +0900 Subject: [PATCH 342/532] Fix test failures/nullability --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 9e7a5e3657..76e6fcecca 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -128,8 +128,7 @@ namespace osu.Game.Rulesets.Scoring private bool beatmapApplied; private readonly Dictionary scoreResultCounts = new Dictionary(); - - private Dictionary? maximumResultCounts; + private readonly Dictionary maximumResultCounts = new Dictionary(); private readonly List hitEvents = new List(); private HitObject? lastHitObject; @@ -419,7 +418,9 @@ namespace osu.Game.Rulesets.Scoring if (storeResults) { maximumScoringValues = currentScoringValues; - maximumResultCounts = new Dictionary(scoreResultCounts); + + maximumResultCounts.Clear(); + maximumResultCounts.AddRange(scoreResultCounts); } scoreResultCounts.Clear(); From dc829334a1b6611f8518d5fa6518f9e498d4de66 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 24 Aug 2022 22:19:32 +0900 Subject: [PATCH 343/532] Update for framework-side changes. --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 2718465b9c..1be2b09ff8 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -56,6 +56,14 @@ namespace osu.Game.Graphics.UserInterface private bool selectionStarted; private double sampleLastPlaybackTime; + private enum SelectionSampleType + { + Character, + Word, + All, + Deselect + } + public OsuTextBox() { Height = 40; @@ -133,16 +141,19 @@ namespace osu.Game.Graphics.UserInterface { base.OnTextSelectionChanged(selectionType); - if (selectionType == TextSelectionType.Word) + switch (selectionType) { - if (!selectionStarted) - playSelectSample(selectionType); - else - playSelectSample(); - } - else - { - playSelectSample(selectionType); + case TextSelectionType.Character: + playSelectSample(SelectionSampleType.Character); + break; + + case TextSelectionType.Word: + playSelectSample(selectionStarted ? SelectionSampleType.Character : SelectionSampleType.Word); + break; + + case TextSelectionType.All: + playSelectSample(SelectionSampleType.All); + break; } selectionStarted = true; @@ -150,15 +161,16 @@ namespace osu.Game.Graphics.UserInterface protected override void OnTextDeselected() { - if (selectionStarted) - playSelectSample(TextSelectionType.Deselect); + base.OnTextDeselected(); + + if (!selectionStarted) return; + + playSelectSample(SelectionSampleType.Deselect); selectionStarted = false; - - base.OnTextDeselected(); } - private void playSelectSample(TextSelectionType selectionType = TextSelectionType.Character) + private void playSelectSample(SelectionSampleType selectionType) { if (Time.Current < sampleLastPlaybackTime + 15) return; @@ -167,15 +179,15 @@ namespace osu.Game.Graphics.UserInterface switch (selectionType) { - case TextSelectionType.All: + case SelectionSampleType.All: channel = selectAllSample?.GetChannel(); break; - case TextSelectionType.Word: + case SelectionSampleType.Word: channel = selectWordSample?.GetChannel(); break; - case TextSelectionType.Deselect: + case SelectionSampleType.Deselect: channel = deselectSample?.GetChannel(); break; From da7f8270da6af8fe7d09ad24954b88500ad4e309 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 24 Aug 2022 22:31:28 +0900 Subject: [PATCH 344/532] Fix incorrect cast --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 1be2b09ff8..60c8dcef21 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -193,7 +193,7 @@ namespace osu.Game.Graphics.UserInterface default: channel = selectCharSample?.GetChannel(); - pitch += (SelectedText.Length / (float)Text.Length) * 0.15f; + pitch += (SelectedText.Length / (double)Text.Length) * 0.15f; break; } From 4de6df71c5749f88ca0a0c323a04be6309ec3d7c Mon Sep 17 00:00:00 2001 From: o-dasher Date: Wed, 24 Aug 2022 20:59:32 -0400 Subject: [PATCH 345/532] No "gameplayClock" usage with playfield update mods --- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 6 +----- .../Mods/OsuModMagnetised.cs | 17 +++++++--------- osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs | 20 +++++++------------ 3 files changed, 15 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index f942538ed3..6772cfe0be 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -31,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Mods private OsuInputManager inputManager = null!; - private IFrameStableClock gameplayClock = null!; - private List replayFrames = null!; private int currentFrame; @@ -41,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods { if (currentFrame == replayFrames.Count - 1) return; - double time = gameplayClock.CurrentTime; + double time = playfield.Clock.CurrentTime; // Very naive implementation of autopilot based on proximity to replay frames. // TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered). @@ -56,8 +54,6 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - gameplayClock = drawableRuleset.FrameStableClock; - // Grab the input manager to disable the user's cursor, and for future use inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; inputManager.AllowUserCursorMovement = false; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index 35a34094d1..fbde9e0491 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; @@ -27,8 +28,6 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 0.5; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) }; - private IFrameStableClock gameplayClock = null!; - [SettingSource("Attraction strength", "How strong the pull is.", 0)] public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f) { @@ -39,8 +38,6 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - gameplayClock = drawableRuleset.FrameStableClock; - // Hide judgment displays and follow points as they won't make any sense. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. drawableRuleset.Playfield.DisplayJudgements.Value = false; @@ -56,27 +53,27 @@ namespace osu.Game.Rulesets.Osu.Mods switch (drawable) { case DrawableHitCircle circle: - easeTo(circle, cursorPos); + easeTo(playfield.Clock, circle, cursorPos); break; case DrawableSlider slider: if (!slider.HeadCircle.Result.HasResult) - easeTo(slider, cursorPos); + easeTo(playfield.Clock, slider, cursorPos); else - easeTo(slider, cursorPos - slider.Ball.DrawPosition); + easeTo(playfield.Clock, slider, cursorPos - slider.Ball.DrawPosition); break; } } } - private void easeTo(DrawableHitObject hitObject, Vector2 destination) + private void easeTo(IFrameBasedClock clock, DrawableHitObject hitObject, Vector2 destination) { 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); + float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, clock.ElapsedFrameTime); + float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime); hitObject.Position = new Vector2(x, y); } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs index 54b594505c..911363a27e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Localisation; +using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; @@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) }; - private IFrameStableClock? gameplayClock; - [SettingSource("Repulsion strength", "How strong the repulsion is.", 0)] public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f) { @@ -39,8 +37,6 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - gameplayClock = drawableRuleset.FrameStableClock; - // Hide judgment displays and follow points as they won't make any sense. // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart. drawableRuleset.Playfield.DisplayJudgements.Value = false; @@ -69,29 +65,27 @@ namespace osu.Game.Rulesets.Osu.Mods switch (drawable) { case DrawableHitCircle circle: - easeTo(circle, destination, cursorPos); + easeTo(playfield.Clock, circle, destination, cursorPos); break; case DrawableSlider slider: if (!slider.HeadCircle.Result.HasResult) - easeTo(slider, destination, cursorPos); + easeTo(playfield.Clock, slider, destination, cursorPos); else - easeTo(slider, destination - slider.Ball.DrawPosition, cursorPos); + easeTo(playfield.Clock, slider, destination - slider.Ball.DrawPosition, cursorPos); break; } } } - private void easeTo(DrawableHitObject hitObject, Vector2 destination, Vector2 cursorPos) + private void easeTo(IFrameBasedClock clock, DrawableHitObject hitObject, Vector2 destination, Vector2 cursorPos) { - Debug.Assert(gameplayClock != null); - double dampLength = Vector2.Distance(hitObject.Position, cursorPos) / (0.04 * RepulsionStrength.Value + 0.04); - float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime); - float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime); + float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, clock.ElapsedFrameTime); + float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime); hitObject.Position = new Vector2(x, y); } From 73f41439aeffd5036fe54038035b67d8d8819aac Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 25 Aug 2022 13:34:58 +0900 Subject: [PATCH 346/532] Remove redundant qualifiers --- osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 63d6e0483b..44ebdad2e4 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -357,13 +357,13 @@ namespace osu.Game.Tests.Rulesets.Scoring private class TestRuleset : Ruleset { - public override IEnumerable GetModsFor(ModType type) => throw new System.NotImplementedException(); + public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException(); - public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new System.NotImplementedException(); + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new NotImplementedException(); - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new System.NotImplementedException(); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException(); - public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new System.NotImplementedException(); + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException(); public override string Description => string.Empty; public override string ShortName => string.Empty; From 17029f0b927b7ec03cc1e193c7fd0dac751e4e09 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 25 Aug 2022 13:58:57 +0900 Subject: [PATCH 347/532] Ensure clones don't reference to MaximumStatistics --- osu.Game/Scoring/ScoreInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 99e0726da7..25a7bad9e8 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -136,6 +136,7 @@ namespace osu.Game.Scoring var clone = (ScoreInfo)this.Detach().MemberwiseClone(); clone.Statistics = new Dictionary(clone.Statistics); + clone.MaximumStatistics = new Dictionary(clone.MaximumStatistics); clone.RealmUser = new RealmUser { OnlineID = RealmUser.OnlineID, From 8eab36f8c9ccb0d14ccdd554a2afb95da2d32789 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 25 Aug 2022 14:02:10 +0900 Subject: [PATCH 348/532] Actually fix possible NaN value --- .../Difficulty/TaikoPerformanceCalculator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 6b1ea58129..95a1e8bc66 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -38,7 +38,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. - effectiveMissCount = Math.Max(1.0, Math.Min(0, 1000.0 / totalSuccessfulHits)) * countMiss; + if (totalSuccessfulHits > 0) + effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss; double multiplier = 1.13; From 1032b2a68c8b1d16b98dc7997d81d4c0d2db2baf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 14:03:25 +0900 Subject: [PATCH 349/532] Fix some `BeatmapCarousel` tests not correctly reinitialising local data per run Closes https://github.com/ppy/osu/issues/19949. --- .../SongSelect/TestSceneBeatmapCarousel.cs | 125 ++++++++++++------ 1 file changed, 84 insertions(+), 41 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index bb9e83a21c..c3e485d56b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -244,8 +244,12 @@ namespace osu.Game.Tests.Visual.SongSelect const int total_set_count = 200; - for (int i = 0; i < total_set_count; i++) - sets.Add(TestResources.CreateTestBeatmapSetInfo()); + AddStep("Populuate beatmap sets", () => + { + sets.Clear(); + for (int i = 0; i < total_set_count; i++) + sets.Add(TestResources.CreateTestBeatmapSetInfo()); + }); loadBeatmaps(sets); @@ -275,8 +279,12 @@ namespace osu.Game.Tests.Visual.SongSelect const int total_set_count = 20; - for (int i = 0; i < total_set_count; i++) - sets.Add(TestResources.CreateTestBeatmapSetInfo(3)); + AddStep("Populuate beatmap sets", () => + { + sets.Clear(); + for (int i = 0; i < total_set_count; i++) + sets.Add(TestResources.CreateTestBeatmapSetInfo(3)); + }); loadBeatmaps(sets); @@ -493,18 +501,23 @@ namespace osu.Game.Tests.Visual.SongSelect const string zzz_string = "zzzzz"; - for (int i = 0; i < 20; i++) + AddStep("Populuate beatmap sets", () => { - var set = TestResources.CreateTestBeatmapSetInfo(); + sets.Clear(); - if (i == 4) - set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string); + for (int i = 0; i < 20; i++) + { + var set = TestResources.CreateTestBeatmapSetInfo(); - if (i == 16) - set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_string); + if (i == 4) + set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string); - sets.Add(set); - } + if (i == 16) + set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_string); + + sets.Add(set); + } + }); loadBeatmaps(sets); @@ -521,21 +534,27 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestSortingStability() { var sets = new List(); + int idOffset = 0; - for (int i = 0; i < 10; i++) + AddStep("Populuate beatmap sets", () => { - var set = TestResources.CreateTestBeatmapSetInfo(); + sets.Clear(); - // only need to set the first as they are a shared reference. - var beatmap = set.Beatmaps.First(); + for (int i = 0; i < 10; i++) + { + var set = TestResources.CreateTestBeatmapSetInfo(); - beatmap.Metadata.Artist = $"artist {i / 2}"; - beatmap.Metadata.Title = $"title {9 - i}"; + // only need to set the first as they are a shared reference. + var beatmap = set.Beatmaps.First(); - sets.Add(set); - } + beatmap.Metadata.Artist = $"artist {i / 2}"; + beatmap.Metadata.Title = $"title {9 - i}"; - int idOffset = sets.First().OnlineID; + sets.Add(set); + } + + idOffset = sets.First().OnlineID; + }); loadBeatmaps(sets); @@ -556,26 +575,32 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestSortingStabilityWithNewItems() { List sets = new List(); + int idOffset = 0; - for (int i = 0; i < 3; i++) + AddStep("Populuate beatmap sets", () => { - var set = TestResources.CreateTestBeatmapSetInfo(3); + sets.Clear(); - // only need to set the first as they are a shared reference. - var beatmap = set.Beatmaps.First(); + for (int i = 0; i < 3; i++) + { + var set = TestResources.CreateTestBeatmapSetInfo(3); - beatmap.Metadata.Artist = "same artist"; - beatmap.Metadata.Title = "same title"; + // only need to set the first as they are a shared reference. + var beatmap = set.Beatmaps.First(); - sets.Add(set); - } + beatmap.Metadata.Artist = "same artist"; + beatmap.Metadata.Title = "same title"; - int idOffset = sets.First().OnlineID; + sets.Add(set); + } + + idOffset = sets.First().OnlineID; + }); loadBeatmaps(sets); AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); - AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b)); + assertOriginalOrderMaintained(); AddStep("Add new item", () => { @@ -590,10 +615,16 @@ namespace osu.Game.Tests.Visual.SongSelect carousel.UpdateBeatmapSet(set); }); - AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b)); + assertOriginalOrderMaintained(); AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false)); - AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b)); + assertOriginalOrderMaintained(); + + void assertOriginalOrderMaintained() + { + AddAssert("Items remain in original order", + () => carousel.BeatmapSets.Select(s => s.OnlineID), () => Is.EqualTo(carousel.BeatmapSets.Select((set, index) => idOffset + index))); + } } [Test] @@ -601,13 +632,18 @@ namespace osu.Game.Tests.Visual.SongSelect { List sets = new List(); - for (int i = 0; i < 3; i++) + AddStep("Populuate beatmap sets", () => { - var set = TestResources.CreateTestBeatmapSetInfo(3); - set.Beatmaps[0].StarRating = 3 - i; - set.Beatmaps[2].StarRating = 6 + i; - sets.Add(set); - } + sets.Clear(); + + for (int i = 0; i < 3; i++) + { + var set = TestResources.CreateTestBeatmapSetInfo(3); + set.Beatmaps[0].StarRating = 3 - i; + set.Beatmaps[2].StarRating = 6 + i; + sets.Add(set); + } + }); loadBeatmaps(sets); @@ -759,8 +795,13 @@ namespace osu.Game.Tests.Visual.SongSelect { List manySets = new List(); - for (int i = 1; i <= 50; i++) - manySets.Add(TestResources.CreateTestBeatmapSetInfo(3)); + AddStep("Populuate beatmap sets", () => + { + manySets.Clear(); + + for (int i = 1; i <= 50; i++) + manySets.Add(TestResources.CreateTestBeatmapSetInfo(3)); + }); loadBeatmaps(manySets); @@ -791,6 +832,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("populate maps", () => { + manySets.Clear(); + for (int i = 0; i < 10; i++) { manySets.Add(TestResources.CreateTestBeatmapSetInfo(3, new[] From 4c45f7d938d90d41c7dd6fb6b6f23e2c51190f4b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 14:26:20 +0900 Subject: [PATCH 350/532] Ensure `FailAnimation` can't be `Start`ed after filters are already removed --- osu.Game/Screens/Play/FailAnimation.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index c1223d7262..312e752cc6 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -105,6 +105,7 @@ namespace osu.Game.Screens.Play } private bool started; + private bool filtersRemoved; /// /// Start the fail animation playing. @@ -113,6 +114,7 @@ namespace osu.Game.Screens.Play public void Start() { if (started) throw new InvalidOperationException("Animation cannot be started more than once."); + if (filtersRemoved) throw new InvalidOperationException("Animation cannot be started after filters have been removed."); started = true; @@ -155,6 +157,11 @@ namespace osu.Game.Screens.Play public void RemoveFilters(bool resetTrackFrequency = true) { + if (filtersRemoved) + return; + + filtersRemoved = true; + if (resetTrackFrequency) track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); From 8f4a953d11a4ef3ddf7eaf3d46b75a2e40f570b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 14:26:42 +0900 Subject: [PATCH 351/532] Ensure fail animation sequence isn't run after the player exit sequence has started --- osu.Game/Screens/Play/Player.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 6827ff04d3..cd7bf6fe9e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; @@ -828,9 +829,17 @@ namespace osu.Game.Screens.Play private bool onFail() { + // Failing after the quit sequence has started may cause weird side effects with the fail animation / effects. + if (GameplayState.HasQuit) + return false; + if (!CheckModsAllowFailure()) return false; + Debug.Assert(!GameplayState.HasFailed); + Debug.Assert(!GameplayState.HasPassed); + Debug.Assert(!GameplayState.HasQuit); + GameplayState.HasFailed = true; updateGameplayState(); From ec60e164392acda3e644726e5774be04df6afd7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 14:35:42 +0900 Subject: [PATCH 352/532] Apply NRT to `FailAnimation` --- osu.Game/Screens/Play/FailAnimation.cs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 312e752cc6..4d3b08cbe0 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -1,14 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Game.Rulesets.UI; using System; using System.Collections.Generic; -using JetBrains.Annotations; using ManagedBass.Fx; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; @@ -34,27 +31,27 @@ namespace osu.Game.Screens.Play /// public class FailAnimation : Container { - public Action OnComplete; + public Action? OnComplete; private readonly DrawableRuleset drawableRuleset; private readonly BindableDouble trackFreq = new BindableDouble(1); private readonly BindableDouble volumeAdjustment = new BindableDouble(0.5); - private Container filters; + private Container filters = null!; - private Box redFlashLayer; + private Box redFlashLayer = null!; - private Track track; + private Track? track; - private AudioFilter failLowPassFilter; - private AudioFilter failHighPassFilter; + private AudioFilter failLowPassFilter = null!; + private AudioFilter failHighPassFilter = null!; private const float duration = 2500; - private Sample failSample; + private Sample? failSample; [Resolved] - private OsuConfigManager config { get; set; } + private OsuConfigManager config { get; set; } = null!; protected override Container Content { get; } = new Container { @@ -66,8 +63,7 @@ namespace osu.Game.Screens.Play /// /// The player screen background, used to adjust appearance on failing. /// - [CanBeNull] - public BackgroundScreen Background { private get; set; } + public BackgroundScreen? Background { private get; set; } public FailAnimation(DrawableRuleset drawableRuleset) { @@ -127,7 +123,7 @@ namespace osu.Game.Screens.Play failHighPassFilter.CutoffTo(300); failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic); - failSample.Play(); + failSample?.Play(); track.AddAdjustment(AdjustableProperty.Frequency, trackFreq); track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); From ad3dd1c700e09b30a64d90e00903ae3dc24f7923 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 14:45:00 +0900 Subject: [PATCH 353/532] Fix a couple of oversights regarding `track` nullability --- osu.Game/Beatmaps/WorkingBeatmap.cs | 1 + osu.Game/Screens/Play/FailAnimation.cs | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 301610ee58..eb914e61d4 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -146,6 +146,7 @@ namespace osu.Game.Beatmaps /// Get the loaded audio track instance. must have first been called. /// This generally happens via MusicController when changing the global beatmap. /// + [NotNull] public Track Track { get diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 4d3b08cbe0..965d3d3454 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play private Box redFlashLayer = null!; - private Track? track; + private Track track = null!; private AudioFilter failLowPassFilter = null!; private AudioFilter failHighPassFilter = null!; @@ -153,15 +153,17 @@ namespace osu.Game.Screens.Play public void RemoveFilters(bool resetTrackFrequency = true) { - if (filtersRemoved) - return; + if (filtersRemoved) return; filtersRemoved = true; - if (resetTrackFrequency) - track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); + if (!started) + return; - track?.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); + if (resetTrackFrequency) + track.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); + + track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); if (filters.Parent == null) return; From a6ed589db4fad90248c56f268c0e94a25c3edd2f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 14:47:32 +0900 Subject: [PATCH 354/532] Remove guard against `RemoveFilters` running more than once It turns out this is required to remove some filters immediate, and some later. Weird. --- osu.Game/Screens/Play/FailAnimation.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 965d3d3454..7275b369c3 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -153,8 +153,6 @@ namespace osu.Game.Screens.Play public void RemoveFilters(bool resetTrackFrequency = true) { - if (filtersRemoved) return; - filtersRemoved = true; if (!started) From 091c51e6646935e0b5616b9b00c11d58462aa3ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 15:00:32 +0900 Subject: [PATCH 355/532] Fix `SliderPath.Version` bindings not being correctly cleaned up on path changes --- .../Sliders/Components/PathControlPointVisualiser.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 0506e8ab8a..0febfba5f2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -142,8 +142,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components case NotifyCollectionChangedAction.Remove: foreach (var point in e.OldItems.Cast()) { - Pieces.RemoveAll(p => p.ControlPoint == point); - Connections.RemoveAll(c => c.ControlPoint == point); + foreach (var piece in Pieces.Where(p => p.ControlPoint == point)) + piece.RemoveAndDisposeImmediately(); + foreach (var connection in Connections.Where(c => c.ControlPoint == point)) + connection.RemoveAndDisposeImmediately(); } // If removing before the end of the path, From 3ca4bdc0878d220c3ba4978c0e5fc2421c037eb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 15:13:38 +0900 Subject: [PATCH 356/532] Add `ToArray()` calls to removal iteration for safety --- .../Sliders/Components/PathControlPointVisualiser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 0febfba5f2..c1908ef44c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -142,9 +142,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components case NotifyCollectionChangedAction.Remove: foreach (var point in e.OldItems.Cast()) { - foreach (var piece in Pieces.Where(p => p.ControlPoint == point)) + foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray()) piece.RemoveAndDisposeImmediately(); - foreach (var connection in Connections.Where(c => c.ControlPoint == point)) + foreach (var connection in Connections.Where(c => c.ControlPoint == point).ToArray()) connection.RemoveAndDisposeImmediately(); } From 6840e906e79e8fe22a8820166ddd01e4cbaeba92 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 25 Aug 2022 09:50:52 +0300 Subject: [PATCH 357/532] `watchedUsers` -> `watchedUsersRefCounts` --- osu.Game/Online/Spectator/SpectatorClient.cs | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index f1ce6258d6..592bae80ba 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -67,7 +67,7 @@ namespace osu.Game.Online.Spectator /// /// A dictionary containing all users currently being watched, with the number of watching components for each user. /// - private readonly Dictionary watchedUsers = new Dictionary(); + private readonly Dictionary watchedUsersRefCounts = new Dictionary(); private readonly BindableDictionary watchedUserStates = new BindableDictionary(); @@ -95,8 +95,8 @@ namespace osu.Game.Online.Spectator if (connected.NewValue) { // get all the users that were previously being watched - var users = new Dictionary(watchedUsers); - watchedUsers.Clear(); + var users = new Dictionary(watchedUsersRefCounts); + watchedUsersRefCounts.Clear(); // resubscribe to watched users. foreach ((int user, int watchers) in users) @@ -125,7 +125,7 @@ namespace osu.Game.Online.Spectator if (!playingUsers.Contains(userId)) playingUsers.Add(userId); - if (watchedUsers.ContainsKey(userId)) + if (watchedUsersRefCounts.ContainsKey(userId)) watchedUserStates[userId] = state; OnUserBeganPlaying?.Invoke(userId, state); @@ -140,7 +140,7 @@ namespace osu.Game.Online.Spectator { playingUsers.Remove(userId); - if (watchedUsers.ContainsKey(userId)) + if (watchedUsersRefCounts.ContainsKey(userId)) watchedUserStates[userId] = state; OnUserFinishedPlaying?.Invoke(userId, state); @@ -236,13 +236,13 @@ namespace osu.Game.Online.Spectator { Debug.Assert(ThreadSafety.IsUpdateThread); - if (watchedUsers.ContainsKey(userId)) + if (watchedUsersRefCounts.ContainsKey(userId)) { - watchedUsers[userId]++; + watchedUsersRefCounts[userId]++; return; } - watchedUsers.Add(userId, 1); + watchedUsersRefCounts.Add(userId, 1); WatchUserInternal(userId); } @@ -252,13 +252,13 @@ namespace osu.Game.Online.Spectator // Todo: This should not be a thing, but requires framework changes. Schedule(() => { - if (watchedUsers.TryGetValue(userId, out int watchers) && watchers > 1) + if (watchedUsersRefCounts.TryGetValue(userId, out int watchers) && watchers > 1) { - watchedUsers[userId]--; + watchedUsersRefCounts[userId]--; return; } - watchedUsers.Remove(userId); + watchedUsersRefCounts.Remove(userId); watchedUserStates.Remove(userId); StopWatchingUserInternal(userId); }); From a5c61d9a5211fb981ee453df40f534a993fc8880 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 16:44:12 +0900 Subject: [PATCH 358/532] Improve understandability of `TestMostInSyncUserIsAudioSource` --- .../TestSceneMultiSpectatorScreen.cs | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 3226eb992d..e9539baf27 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -165,11 +165,11 @@ namespace osu.Game.Tests.Visual.Multiplayer sendFrames(PLAYER_1_ID, 40); sendFrames(PLAYER_2_ID, 20); - checkPaused(PLAYER_2_ID, true); + waitUntilPaused(PLAYER_2_ID, true); checkPausedInstant(PLAYER_1_ID, false); AddAssert("master clock still running", () => this.ChildrenOfType().Single().IsRunning); - checkPaused(PLAYER_1_ID, true); + waitUntilPaused(PLAYER_1_ID, true); AddUntilStep("master clock paused", () => !this.ChildrenOfType().Single().IsRunning); } @@ -222,11 +222,11 @@ namespace osu.Game.Tests.Visual.Multiplayer checkPausedInstant(PLAYER_2_ID, false); // Eventually player 2 will pause, player 1 must remain running. - checkPaused(PLAYER_2_ID, true); + waitUntilPaused(PLAYER_2_ID, true); checkPausedInstant(PLAYER_1_ID, false); // Eventually both players will run out of frames and should pause. - checkPaused(PLAYER_1_ID, true); + waitUntilPaused(PLAYER_1_ID, true); checkPausedInstant(PLAYER_2_ID, true); // Send more frames for the first player only. Player 1 should start playing with player 2 remaining paused. @@ -253,7 +253,7 @@ namespace osu.Game.Tests.Visual.Multiplayer checkPausedInstant(PLAYER_2_ID, false); // Eventually player 2 will run out of frames and should pause. - checkPaused(PLAYER_2_ID, true); + waitUntilPaused(PLAYER_2_ID, true); AddWaitStep("wait a few more frames", 10); // Send more frames for player 2. It should unpause. @@ -271,21 +271,28 @@ namespace osu.Game.Tests.Visual.Multiplayer start(new[] { PLAYER_1_ID, PLAYER_2_ID }); loadSpectateScreen(); + // With no frames, the synchronisation state will be TooFarAhead. + // In this state, all players should be muted. assertMuted(PLAYER_1_ID, true); assertMuted(PLAYER_2_ID, true); - sendFrames(PLAYER_1_ID); - sendFrames(PLAYER_2_ID, 20); - checkPaused(PLAYER_1_ID, false); - assertOneNotMuted(); + // Send frames for both players, with more frames for player 2. + sendFrames(PLAYER_1_ID, 5); + sendFrames(PLAYER_2_ID, 40); - checkPaused(PLAYER_1_ID, true); + // While both players are running, one of them should be un-muted. + waitUntilPaused(PLAYER_1_ID, false); + assertOnePlayerNotMuted(); + + // After player 1 runs out of frames, the un-muted player should always be player 2. + waitUntilPaused(PLAYER_1_ID, true); + waitUntilPaused(PLAYER_2_ID, false); assertMuted(PLAYER_1_ID, true); assertMuted(PLAYER_2_ID, false); sendFrames(PLAYER_1_ID, 100); waitForCatchup(PLAYER_1_ID); - checkPaused(PLAYER_2_ID, true); + waitUntilPaused(PLAYER_2_ID, true); assertMuted(PLAYER_1_ID, false); assertMuted(PLAYER_2_ID, true); @@ -319,7 +326,7 @@ namespace osu.Game.Tests.Visual.Multiplayer sendFrames(PLAYER_1_ID, 300); AddWaitStep("wait maximum start delay seconds", (int)(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction)); - checkPaused(PLAYER_1_ID, false); + waitUntilPaused(PLAYER_1_ID, false); sendFrames(PLAYER_2_ID, 300); AddUntilStep("player 2 playing from correct point in time", () => getPlayer(PLAYER_2_ID).ChildrenOfType().Single().FrameStableClock.CurrentTime > 30000); @@ -456,18 +463,18 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } - private void checkPaused(int userId, bool state) + private void waitUntilPaused(int userId, bool state) => AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().IsRunning != state); private void checkPausedInstant(int userId, bool state) { - checkPaused(userId, state); + waitUntilPaused(userId, state); // Todo: The following should work, but is broken because SpectatorScreen retrieves the WorkingBeatmap via the BeatmapManager, bypassing the test scene clock and running real-time. // AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); } - private void assertOneNotMuted() => AddAssert("one player not muted", () => spectatorScreen.ChildrenOfType().Count(p => !p.Mute) == 1); + private void assertOnePlayerNotMuted() => AddAssert("one player not muted", () => spectatorScreen.ChildrenOfType().Count(p => !p.Mute) == 1); private void assertMuted(int userId, bool muted) => AddAssert($"{userId} {(muted ? "is" : "is not")} muted", () => getInstance(userId).Mute == muted); From e2e10a8f267b82baae1e693dcce9d07b847dd569 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 25 Aug 2022 17:14:35 +0900 Subject: [PATCH 359/532] Add some explanatory comments to conditions --- osu.Game/Rulesets/Scoring/HitResult.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index e6aba4a70e..0c898e9543 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -175,6 +175,7 @@ namespace osu.Game.Rulesets.Scoring /// public static bool AffectsAccuracy(this HitResult result) { + // LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result. if (result == HitResult.LegacyComboIncrease) return false; @@ -186,6 +187,7 @@ namespace osu.Game.Rulesets.Scoring /// public static bool IsBasic(this HitResult result) { + // LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result. if (result == HitResult.LegacyComboIncrease) return false; @@ -250,6 +252,7 @@ namespace osu.Game.Rulesets.Scoring /// public static bool IsScorable(this HitResult result) { + // LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output. if (result == HitResult.LegacyComboIncrease) return true; From 9bca7223f67e09626a0d1fd927f4328ec35248a8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 25 Aug 2022 17:16:30 +0900 Subject: [PATCH 360/532] Adjust xmldoc to better explain score contribution --- osu.Game/Rulesets/Scoring/HitResult.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 0c898e9543..5047fdea82 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Scoring IgnoreHit, /// - /// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not contribute to score. + /// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not affect the base score (does not affect accuracy). /// /// /// DO NOT USE. From ddb434f47a314c38bdb5892c4029c9835d91ac87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 17:30:13 +0900 Subject: [PATCH 361/532] Rename asserts to method names to make it easier to track in logs --- .../TestSceneMultiSpectatorScreen.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index e9539baf27..7235e10642 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -278,7 +278,7 @@ namespace osu.Game.Tests.Visual.Multiplayer // Send frames for both players, with more frames for player 2. sendFrames(PLAYER_1_ID, 5); - sendFrames(PLAYER_2_ID, 40); + sendFrames(PLAYER_2_ID, 20); // While both players are running, one of them should be un-muted. waitUntilPaused(PLAYER_1_ID, false); @@ -452,6 +452,10 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + /// + /// Send new frames on behalf of a user. + /// Frames will last for count * 100 milliseconds. + /// private void sendFrames(int userId, int count = 10) => sendFrames(new[] { userId }, count); private void sendFrames(int[] userIds, int count = 10) @@ -464,7 +468,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } private void waitUntilPaused(int userId, bool state) - => AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().IsRunning != state); + => AddUntilStep($"{nameof(waitUntilPaused)}({userId}, {state})", () => getPlayer(userId).ChildrenOfType().First().IsRunning != state); private void checkPausedInstant(int userId, bool state) { @@ -474,19 +478,19 @@ namespace osu.Game.Tests.Visual.Multiplayer // AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); } - private void assertOnePlayerNotMuted() => AddAssert("one player not muted", () => spectatorScreen.ChildrenOfType().Count(p => !p.Mute) == 1); + private void assertOnePlayerNotMuted() => AddAssert(nameof(assertOnePlayerNotMuted), () => spectatorScreen.ChildrenOfType().Count(p => !p.Mute) == 1); private void assertMuted(int userId, bool muted) - => AddAssert($"{userId} {(muted ? "is" : "is not")} muted", () => getInstance(userId).Mute == muted); + => AddAssert($"{nameof(assertMuted)}({userId}, {muted})", () => getInstance(userId).Mute == muted); private void assertRunning(int userId) - => AddAssert($"{userId} clock running", () => getInstance(userId).SpectatorPlayerClock.IsRunning); + => AddAssert($"{nameof(assertRunning)}({userId})", () => getInstance(userId).SpectatorPlayerClock.IsRunning); private void assertNotCatchingUp(int userId) - => AddAssert($"{userId} in sync", () => !getInstance(userId).SpectatorPlayerClock.IsCatchingUp); + => AddAssert($"{nameof(assertNotCatchingUp)}({userId})", () => !getInstance(userId).SpectatorPlayerClock.IsCatchingUp); private void waitForCatchup(int userId) - => AddUntilStep($"{userId} not catching up", () => !getInstance(userId).SpectatorPlayerClock.IsCatchingUp); + => AddUntilStep($"{nameof(waitForCatchup)}({userId})", () => !getInstance(userId).SpectatorPlayerClock.IsCatchingUp); private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType().Single(); From a8c699610a2e2544e670eb955f1b45d1c2425407 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 17:58:15 +0900 Subject: [PATCH 362/532] Fix lead in tests not waiting for player to start running The tests are only meant to ensure that gameplay eventually starts. The case where failures can occur is where the master clock is behind the player clock (due to being in lead-in time). Because the test is running in real-time, it can take arbitrary amounts of time to catch up. If it took too long, the test would fail. --- .../Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 7235e10642..3498ca5e6d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -400,7 +400,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for clock running", () => getInstance(PLAYER_1_ID).SpectatorPlayerClock.IsRunning); assertNotCatchingUp(PLAYER_1_ID); - assertRunning(PLAYER_1_ID); + waitForRunning(PLAYER_1_ID); } private void loadSpectateScreen(bool waitForPlayerLoad = true, Action? applyToBeatmap = null) @@ -486,6 +486,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertRunning(int userId) => AddAssert($"{nameof(assertRunning)}({userId})", () => getInstance(userId).SpectatorPlayerClock.IsRunning); + private void waitForRunning(int userId) + => AddUntilStep($"{nameof(waitForRunning)}({userId})", () => getInstance(userId).SpectatorPlayerClock.IsRunning); + private void assertNotCatchingUp(int userId) => AddAssert($"{nameof(assertNotCatchingUp)}({userId})", () => !getInstance(userId).SpectatorPlayerClock.IsCatchingUp); From 2d2bfac5e9c955b3047dca66c30635492a9d9e9d Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Thu, 25 Aug 2022 17:49:38 +0800 Subject: [PATCH 363/532] used `firstHitObject.Samples` as samples for `mergedHitObject` --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 061c5008c5..2c5bbdb279 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -371,6 +371,7 @@ namespace osu.Game.Rulesets.Osu.Edit Position = firstHitObject.Position, NewCombo = firstHitObject.NewCombo, SampleControlPoint = firstHitObject.SampleControlPoint, + Samples = firstHitObject.Samples, }; if (mergedHitObject.Path.ControlPoints.Count == 0) From ae38c9e58d94a036a33acdafc04c3f620f91400a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 19:18:17 +0900 Subject: [PATCH 364/532] 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 17a6178641..bff3627af7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 0613db891b..5fc69e475f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index bf1e4e350c..f763e411be 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - + From 16fee7ac1c402b751a9b0a57b10ba1892841b4de Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Thu, 25 Aug 2022 19:31:47 +0800 Subject: [PATCH 365/532] add visual test --- .../Editor/TestSceneObjectMerging.cs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index b68231ce64..7a5b6022b7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -168,6 +168,99 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner)); } + [Test] + public void TestSimpleMergeHitSound() + { + HitCircle? circle1 = null; + HitCircle? circle2 = null; + double sliderStartTime = 0; + + AddStep("select first two circles", () => + { + circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle); + circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1); + EditorClock.Seek(circle1.StartTime); + EditorBeatmap.SelectedHitObjects.Add(circle1); + EditorBeatmap.SelectedHitObjects.Add(circle2); + sliderStartTime = circle1.StartTime; + }); + + mergeSelection(); + + AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( + (pos: circle1.Position, pathType: PathType.Linear), + (pos: circle2.Position, pathType: null))); + + AddStep("start editor clock", () => + { + EditorClock.Seek(sliderStartTime); + EditorClock.Start(); + }); + + AddStep("stop editor clock", () => + { + EditorClock.Stop(); + }); + + AddStep("undo", () => Editor.Undo()); + AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && objectsRestored(circle1, circle2)); + } + + [Test] + public void TestMergeCircleSliderHitsound() + { + HitCircle? circle1 = null; + Slider? slider = null; + HitCircle? circle2 = null; + double sliderStartTime = 0; + + AddStep("select a circle, slider, circle", () => + { + circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle); + slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > circle1.StartTime); + circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h.StartTime > slider.StartTime); + EditorClock.Seek(circle1.StartTime); + EditorBeatmap.SelectedHitObjects.Add(circle1); + EditorBeatmap.SelectedHitObjects.Add(slider); + EditorBeatmap.SelectedHitObjects.Add(circle2); + sliderStartTime = circle1.StartTime; + }); + + mergeSelection(); + + AddAssert("slider created", () => + { + if (circle1 is null || circle2 is null || slider is null) + return false; + + var controlPoints = slider.Path.ControlPoints; + (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2]; + args[0] = (circle1.Position, PathType.Linear); + + for (int i = 0; i < controlPoints.Count; i++) + { + args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.Linear : controlPoints[i].Type); + } + + args[^1] = (circle2.Position, null); + return sliderCreatedFor(args); + }); + + AddStep("start editor clock", () => + { + EditorClock.Seek(sliderStartTime); + EditorClock.Start(); + }); + + AddStep("stop editor clock", () => + { + EditorClock.Stop(); + }); + + AddStep("undo", () => Editor.Undo()); + AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && objectsRestored(circle1, circle2)); + } + private void mergeSelection() { AddStep("merge selection", () => From a546aa267390b38402d064578e4749f93096ec51 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 21:33:33 +0900 Subject: [PATCH 366/532] Clamp `SpectatorPlayerClock`'s elapsed calculation to avoid player clocks getting too far ahead --- .../OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs index 62731c6903..45615d4e19 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs @@ -80,7 +80,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate // When in catch-up mode, the source is usually not running. // In such a case, its elapsed time may be zero, which would cause catch-up to get stuck. // To avoid this, use a constant 16ms elapsed time for now. Probably not too correct, but this whole logic isn't too correct anyway. - double elapsedSource = masterClock.ElapsedFrameTime != 0 ? masterClock.ElapsedFrameTime : 16; + // Clamping is required to ensure that player clocks don't get too far ahead if ProcessFrame is run multiple times. + double elapsedSource = masterClock.ElapsedFrameTime != 0 ? masterClock.ElapsedFrameTime : Math.Clamp(masterClock.CurrentTime - CurrentTime, 0, 16); double elapsed = elapsedSource * Rate; CurrentTime += elapsed; From a260d7872d58dda2249a941f3dd58a96eceb5764 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 21:50:10 +0900 Subject: [PATCH 367/532] 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 17a6178641..bff3627af7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 0613db891b..5fc69e475f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index bf1e4e350c..f763e411be 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - + From b0e7f63361aa072841a0614b6454aeb5aa243158 Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 26 Aug 2022 12:34:33 +1000 Subject: [PATCH 368/532] Update angle multiplier to nerf repeated angles --- .../Evaluators/FlashlightEvaluator.cs | 28 ++++++++++++------- .../Difficulty/Skills/Flashlight.cs | 2 +- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 9d2696c978..8cf8190554 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -7,6 +7,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; +using osu.Framework.Utils; namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const double min_velocity = 0.5; private const double slider_multiplier = 1.3; - private const double min_grid_multiplier = 0.35; + private const double min_angle_multiplier = 0.2; /// /// Evaluates the difficulty of memorising and hitting an object, based on: @@ -46,6 +47,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators OsuDifficultyHitObject lastObj = osuCurrent; + int angleRepeatCount = 0; + + // We want to round angles to make abusing the nerf a bit harder. + double initialRoundedAngle = 0.0; + if (osuCurrent.Angle != null) + initialRoundedAngle = Math.Round(MathUtils.RadiansToDegrees(osuCurrent.Angle.Value) / 2.0) * 2.0; + // This is iterating backwards in time from the current object. for (int i = 0; i < Math.Min(current.Index, 10); i++) { @@ -69,6 +77,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - osuCurrent.OpacityAt(currentHitObject.StartTime, hidden)); result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime; + + if (currentObj.Angle != null && osuCurrent.Angle != null) { + double roundedAngle = Math.Round(MathUtils.RadiansToDegrees(currentObj.Angle.Value) / 2.0) * 2.0; + + if (roundedAngle == initialRoundedAngle) + angleRepeatCount++; + } } lastObj = currentObj; @@ -80,15 +95,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (hidden) result *= 1.0 + hidden_bonus; - // Nerf patterns with angles that are commonly used in grid maps. - // 0 deg, 60 deg, 120 deg and 180 deg are commonly used in hexgrid maps. - // 0 deg, 45 deg, 90 deg, 135 deg and 180 deg are commonly used in squaregrid maps. - if (osuCurrent.Angle != null) - { - double hexgridMultiplier = 1.0 - Math.Pow(Math.Cos((180 / 60.0) * (double)(osuCurrent.Angle)), 20.0); - double squaregridMultiplier = 1.0 - Math.Pow(Math.Cos((180 / 45.0) * (double)(osuCurrent.Angle)), 20.0); - result *= (1.0 - min_grid_multiplier) * hexgridMultiplier * squaregridMultiplier + min_grid_multiplier; - } + // Nerf patterns with repeated angles. + result *= min_angle_multiplier + (1.0 - min_angle_multiplier) / (angleRepeatCount + 1.0); double sliderBonus = 0.0; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 03130031ea..84ef109598 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills hasHiddenMod = mods.Any(m => m is OsuModHidden); } - private double skillMultiplier => 0.06; + private double skillMultiplier => 0.05; private double strainDecayBase => 0.15; private double currentStrain; From 6651e76e2e9215ebfe700d5fcb4720c2ae265308 Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 26 Aug 2022 12:37:56 +1000 Subject: [PATCH 369/532] Remove whitespace --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 8cf8190554..f3953bdb79 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -80,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (currentObj.Angle != null && osuCurrent.Angle != null) { double roundedAngle = Math.Round(MathUtils.RadiansToDegrees(currentObj.Angle.Value) / 2.0) * 2.0; - + if (roundedAngle == initialRoundedAngle) angleRepeatCount++; } From d8854413cbbcf0607edd6093c9b0cfea8935a3ed Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 26 Aug 2022 12:38:36 +1000 Subject: [PATCH 370/532] Add newline --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index f3953bdb79..86b6170d13 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -78,7 +78,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime; - if (currentObj.Angle != null && osuCurrent.Angle != null) { + if (currentObj.Angle != null && osuCurrent.Angle != null) + { double roundedAngle = Math.Round(MathUtils.RadiansToDegrees(currentObj.Angle.Value) / 2.0) * 2.0; if (roundedAngle == initialRoundedAngle) From 9862b79b47595d383e769f63d8f0dd9a207008c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 15:20:09 +0900 Subject: [PATCH 371/532] Fix typo in long comment --- osu.Game/Screens/Play/GameplayClockContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 21fffb2992..21f6814f7c 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -92,7 +92,7 @@ namespace osu.Game.Screens.Play // This accounts for the clock source potentially taking time to enter a completely stopped state Seek(GameplayClock.CurrentTime); - // The case which cause this to be added is FrameStabilityContainer, which manages its own current and elapsed time. + // The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time. // Because we generally update our own current time quicker than children can query it (via Start/Seek/Update), // this means that the first frame ever exposed to children may have a non-zero current time. // From ed0843aa84628501a937b787cbda995a88add486 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 16:46:22 +0900 Subject: [PATCH 372/532] Reword xmldoc regarding final clock source to read better --- osu.Game/Screens/Play/GameplayClockContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 21f6814f7c..1ae393d06a 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Play /// /// The adjustable source clock used for gameplay. Should be used for seeks and clock control. - /// This is the final clock exposed to gameplay components as an . + /// This is the final source exposed to gameplay components via delegation in this class. /// protected readonly FramedBeatmapClock GameplayClock; From 9050f54681dbf6acca1167c824c9fd30a3344c64 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 16:56:03 +0900 Subject: [PATCH 373/532] Split out test assertion methods to read better --- .../TestSceneMultiSpectatorScreen.cs | 84 ++++++++++--------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 3498ca5e6d..a11a67aebd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -165,11 +165,11 @@ namespace osu.Game.Tests.Visual.Multiplayer sendFrames(PLAYER_1_ID, 40); sendFrames(PLAYER_2_ID, 20); - waitUntilPaused(PLAYER_2_ID, true); - checkPausedInstant(PLAYER_1_ID, false); + waitUntilPaused(PLAYER_2_ID); + checkRunningInstant(PLAYER_1_ID); AddAssert("master clock still running", () => this.ChildrenOfType().Single().IsRunning); - waitUntilPaused(PLAYER_1_ID, true); + waitUntilPaused(PLAYER_1_ID); AddUntilStep("master clock paused", () => !this.ChildrenOfType().Single().IsRunning); } @@ -181,13 +181,13 @@ namespace osu.Game.Tests.Visual.Multiplayer // Send frames for one player only, both should remain paused. sendFrames(PLAYER_1_ID, 20); - checkPausedInstant(PLAYER_1_ID, true); - checkPausedInstant(PLAYER_2_ID, true); + checkPausedInstant(PLAYER_1_ID); + checkPausedInstant(PLAYER_2_ID); // Send frames for the other player, both should now start playing. sendFrames(PLAYER_2_ID, 20); - checkPausedInstant(PLAYER_1_ID, false); - checkPausedInstant(PLAYER_2_ID, false); + checkRunningInstant(PLAYER_1_ID); + checkRunningInstant(PLAYER_2_ID); } [Test] @@ -198,15 +198,15 @@ namespace osu.Game.Tests.Visual.Multiplayer // Send frames for one player only, both should remain paused. sendFrames(PLAYER_1_ID, 1000); - checkPausedInstant(PLAYER_1_ID, true); - checkPausedInstant(PLAYER_2_ID, true); + checkPausedInstant(PLAYER_1_ID); + checkPausedInstant(PLAYER_2_ID); // Wait for the start delay seconds... AddWaitStep("wait maximum start delay seconds", (int)(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction)); // Player 1 should start playing by itself, player 2 should remain paused. - checkPausedInstant(PLAYER_1_ID, false); - checkPausedInstant(PLAYER_2_ID, true); + checkRunningInstant(PLAYER_1_ID); + checkPausedInstant(PLAYER_2_ID); } [Test] @@ -218,26 +218,26 @@ namespace osu.Game.Tests.Visual.Multiplayer // Send initial frames for both players. A few more for player 1. sendFrames(PLAYER_1_ID, 20); sendFrames(PLAYER_2_ID); - checkPausedInstant(PLAYER_1_ID, false); - checkPausedInstant(PLAYER_2_ID, false); + checkRunningInstant(PLAYER_1_ID); + checkRunningInstant(PLAYER_2_ID); // Eventually player 2 will pause, player 1 must remain running. - waitUntilPaused(PLAYER_2_ID, true); - checkPausedInstant(PLAYER_1_ID, false); + waitUntilPaused(PLAYER_2_ID); + checkRunningInstant(PLAYER_1_ID); // Eventually both players will run out of frames and should pause. - waitUntilPaused(PLAYER_1_ID, true); - checkPausedInstant(PLAYER_2_ID, true); + waitUntilPaused(PLAYER_1_ID); + checkPausedInstant(PLAYER_2_ID); // Send more frames for the first player only. Player 1 should start playing with player 2 remaining paused. sendFrames(PLAYER_1_ID, 20); - checkPausedInstant(PLAYER_2_ID, true); - checkPausedInstant(PLAYER_1_ID, false); + checkPausedInstant(PLAYER_2_ID); + checkRunningInstant(PLAYER_1_ID); // Send more frames for the second player. Both should be playing sendFrames(PLAYER_2_ID, 20); - checkPausedInstant(PLAYER_2_ID, false); - checkPausedInstant(PLAYER_1_ID, false); + checkRunningInstant(PLAYER_2_ID); + checkRunningInstant(PLAYER_1_ID); } [Test] @@ -249,16 +249,16 @@ namespace osu.Game.Tests.Visual.Multiplayer // Send initial frames for both players. A few more for player 1. sendFrames(PLAYER_1_ID, 1000); sendFrames(PLAYER_2_ID, 30); - checkPausedInstant(PLAYER_1_ID, false); - checkPausedInstant(PLAYER_2_ID, false); + checkRunningInstant(PLAYER_1_ID); + checkRunningInstant(PLAYER_2_ID); // Eventually player 2 will run out of frames and should pause. - waitUntilPaused(PLAYER_2_ID, true); + waitUntilPaused(PLAYER_2_ID); AddWaitStep("wait a few more frames", 10); // Send more frames for player 2. It should unpause. sendFrames(PLAYER_2_ID, 1000); - checkPausedInstant(PLAYER_2_ID, false); + checkRunningInstant(PLAYER_2_ID); // Player 2 should catch up to player 1 after unpausing. waitForCatchup(PLAYER_2_ID); @@ -281,18 +281,18 @@ namespace osu.Game.Tests.Visual.Multiplayer sendFrames(PLAYER_2_ID, 20); // While both players are running, one of them should be un-muted. - waitUntilPaused(PLAYER_1_ID, false); + waitUntilRunning(PLAYER_1_ID); assertOnePlayerNotMuted(); // After player 1 runs out of frames, the un-muted player should always be player 2. - waitUntilPaused(PLAYER_1_ID, true); - waitUntilPaused(PLAYER_2_ID, false); + waitUntilPaused(PLAYER_1_ID); + waitUntilRunning(PLAYER_2_ID); assertMuted(PLAYER_1_ID, true); assertMuted(PLAYER_2_ID, false); sendFrames(PLAYER_1_ID, 100); waitForCatchup(PLAYER_1_ID); - waitUntilPaused(PLAYER_2_ID, true); + waitUntilPaused(PLAYER_2_ID); assertMuted(PLAYER_1_ID, false); assertMuted(PLAYER_2_ID, true); @@ -326,7 +326,7 @@ namespace osu.Game.Tests.Visual.Multiplayer sendFrames(PLAYER_1_ID, 300); AddWaitStep("wait maximum start delay seconds", (int)(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction)); - waitUntilPaused(PLAYER_1_ID, false); + waitUntilRunning(PLAYER_1_ID); sendFrames(PLAYER_2_ID, 300); AddUntilStep("player 2 playing from correct point in time", () => getPlayer(PLAYER_2_ID).ChildrenOfType().Single().FrameStableClock.CurrentTime > 30000); @@ -400,7 +400,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for clock running", () => getInstance(PLAYER_1_ID).SpectatorPlayerClock.IsRunning); assertNotCatchingUp(PLAYER_1_ID); - waitForRunning(PLAYER_1_ID); + waitUntilRunning(PLAYER_1_ID); } private void loadSpectateScreen(bool waitForPlayerLoad = true, Action? applyToBeatmap = null) @@ -467,12 +467,17 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } - private void waitUntilPaused(int userId, bool state) - => AddUntilStep($"{nameof(waitUntilPaused)}({userId}, {state})", () => getPlayer(userId).ChildrenOfType().First().IsRunning != state); - - private void checkPausedInstant(int userId, bool state) + private void checkRunningInstant(int userId) { - waitUntilPaused(userId, state); + waitUntilRunning(userId); + + // Todo: The following should work, but is broken because SpectatorScreen retrieves the WorkingBeatmap via the BeatmapManager, bypassing the test scene clock and running real-time. + // AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); + } + + private void checkPausedInstant(int userId) + { + waitUntilPaused(userId); // Todo: The following should work, but is broken because SpectatorScreen retrieves the WorkingBeatmap via the BeatmapManager, bypassing the test scene clock and running real-time. // AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); @@ -486,8 +491,11 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertRunning(int userId) => AddAssert($"{nameof(assertRunning)}({userId})", () => getInstance(userId).SpectatorPlayerClock.IsRunning); - private void waitForRunning(int userId) - => AddUntilStep($"{nameof(waitForRunning)}({userId})", () => getInstance(userId).SpectatorPlayerClock.IsRunning); + private void waitUntilPaused(int userId) + => AddUntilStep($"{nameof(waitUntilPaused)}({userId})", () => !getPlayer(userId).ChildrenOfType().First().IsRunning); + + private void waitUntilRunning(int userId) + => AddUntilStep($"{nameof(waitUntilRunning)}({userId})", () => getPlayer(userId).ChildrenOfType().First().IsRunning); private void assertNotCatchingUp(int userId) => AddAssert($"{nameof(assertNotCatchingUp)}({userId})", () => !getInstance(userId).SpectatorPlayerClock.IsCatchingUp); From 31e459364bd2eb6a5c6879447fbd596dda0f5b52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Aug 2022 14:11:42 +0900 Subject: [PATCH 374/532] Use `FramedBeatmapClock` in `EditorClock` --- osu.Game/Screens/Edit/EditorClock.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index c3b29afd30..d55f2c0bf6 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -4,10 +4,12 @@ #nullable disable using System; +using System.Diagnostics; using System.Linq; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Transforms; using osu.Framework.Timing; using osu.Framework.Utils; @@ -19,7 +21,7 @@ namespace osu.Game.Screens.Edit /// /// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor. /// - public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock + public class EditorClock : CompositeComponent, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock { public IBindable Track => track; @@ -33,7 +35,7 @@ namespace osu.Game.Screens.Edit private readonly BindableBeatDivisor beatDivisor; - private readonly DecoupleableInterpolatingFramedClock underlyingClock; + private readonly FramedBeatmapClock underlyingClock; private bool playbackFinished; @@ -52,7 +54,8 @@ namespace osu.Game.Screens.Edit this.beatDivisor = beatDivisor ?? new BindableBeatDivisor(); - underlyingClock = new DecoupleableInterpolatingFramedClock(); + underlyingClock = new FramedBeatmapClock(applyOffsets: true); + AddInternal(underlyingClock); } /// @@ -219,6 +222,9 @@ namespace osu.Game.Screens.Edit public void ProcessFrame() { + // EditorClock wasn't being added in many places. This gives us more certainty that it is. + Debug.Assert(underlyingClock.LoadState > LoadState.NotLoaded); + underlyingClock.ProcessFrame(); playbackFinished = CurrentTime >= TrackLength; From fec744a7fe5b4d44fdcd801ef63ad14d63ee334f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 17:44:33 +0900 Subject: [PATCH 375/532] Add global `FramedBeatmapClock` for `BeatSyncProvider` components --- osu.Game/OsuGameBase.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8d5c58d5f0..27ea3a76ae 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -185,6 +185,12 @@ namespace osu.Game private RealmAccess realm; + /// + /// For now, this is used as a source specifically for beat synced components. + /// Going forward, it could potentially be used as the single source-of-truth for beatmap timing. + /// + private readonly FramedBeatmapClock beatmapClock = new FramedBeatmapClock(true); + protected override Container Content => content; private Container content; @@ -368,10 +374,15 @@ namespace osu.Game AddInternal(MusicController = new MusicController()); dependencies.CacheAs(MusicController); + MusicController.TrackChanged += onTrackChanged; + AddInternal(beatmapClock); + Ruleset.BindValueChanged(onRulesetChanged); Beatmap.BindValueChanged(onBeatmapChanged); } + private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) => beatmapClock.ChangeSource(beatmap.Track); + protected virtual void InitialiseFonts() { AddFont(Resources, @"Fonts/osuFont"); @@ -587,7 +598,7 @@ namespace osu.Game } ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.BeatmapLoaded ? Beatmap.Value.Beatmap.ControlPointInfo : null; - IClock IBeatSyncProvider.Clock => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track : (IClock)null; + IClock IBeatSyncProvider.Clock => beatmapClock; ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : ChannelAmplitudes.Empty; } } From 78717956d536df4ba1bfea7e37f01920fb1a9f81 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Fri, 26 Aug 2022 16:55:18 +0800 Subject: [PATCH 376/532] add visual test --- .../Editor/TestSceneObjectMerging.cs | 88 ++++++------------- 1 file changed, 25 insertions(+), 63 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 7a5b6022b7..75a2361732 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osuTK; using osuTK.Input; +using System.Collections.Generic; namespace osu.Game.Rulesets.Osu.Tests.Editor { @@ -169,11 +170,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } [Test] - public void TestSimpleMergeHitSound() + public void TestSimpleMergeSample() { HitCircle? circle1 = null; HitCircle? circle2 = null; - double sliderStartTime = 0; + AddStep("select first two circles", () => { @@ -182,83 +183,31 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor EditorClock.Seek(circle1.StartTime); EditorBeatmap.SelectedHitObjects.Add(circle1); EditorBeatmap.SelectedHitObjects.Add(circle2); - sliderStartTime = circle1.StartTime; }); mergeSelection(); - AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( - (pos: circle1.Position, pathType: PathType.Linear), - (pos: circle2.Position, pathType: null))); - - AddStep("start editor clock", () => - { - EditorClock.Seek(sliderStartTime); - EditorClock.Start(); - }); - - AddStep("stop editor clock", () => - { - EditorClock.Stop(); - }); - - AddStep("undo", () => Editor.Undo()); - AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && objectsRestored(circle1, circle2)); + AddAssert("samples exist", () => sliderSampleExist()); } [Test] - public void TestMergeCircleSliderHitsound() + public void TestSliderCircleMergeSample() { - HitCircle? circle1 = null; Slider? slider = null; - HitCircle? circle2 = null; - double sliderStartTime = 0; + HitCircle? circle = null; - AddStep("select a circle, slider, circle", () => + AddStep("select a slider followed by a circle", () => { - circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle); - slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > circle1.StartTime); - circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h.StartTime > slider.StartTime); - EditorClock.Seek(circle1.StartTime); - EditorBeatmap.SelectedHitObjects.Add(circle1); + slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider); + circle = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h.StartTime > slider.StartTime); + EditorClock.Seek(slider.StartTime); EditorBeatmap.SelectedHitObjects.Add(slider); - EditorBeatmap.SelectedHitObjects.Add(circle2); - sliderStartTime = circle1.StartTime; + EditorBeatmap.SelectedHitObjects.Add(circle); }); mergeSelection(); - AddAssert("slider created", () => - { - if (circle1 is null || circle2 is null || slider is null) - return false; - - var controlPoints = slider.Path.ControlPoints; - (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2]; - args[0] = (circle1.Position, PathType.Linear); - - for (int i = 0; i < controlPoints.Count; i++) - { - args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.Linear : controlPoints[i].Type); - } - - args[^1] = (circle2.Position, null); - return sliderCreatedFor(args); - }); - - AddStep("start editor clock", () => - { - EditorClock.Seek(sliderStartTime); - EditorClock.Start(); - }); - - AddStep("stop editor clock", () => - { - EditorClock.Stop(); - }); - - AddStep("undo", () => Editor.Undo()); - AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && objectsRestored(circle1, circle2)); + AddAssert("samples exist", () => sliderSampleExist()); } private void mergeSelection() @@ -302,5 +251,18 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor return true; } + + private bool sliderSampleExist() + { + if (EditorBeatmap.SelectedHitObjects.Count != 1) + return false; + + var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); + + if (mergedSlider.Samples[0] is null) + return false; + + return true; + } } } From 12d6d6793cd5eba002c0b39a976dc15c8c8955b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 18:08:39 +0900 Subject: [PATCH 377/532] Move `EditorClock` processing to `Update` and always decouple --- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/Edit/EditorClock.cs | 38 ++++++++++++---------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a7cbe1f1ad..ad8fda7ad0 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -220,7 +220,7 @@ namespace osu.Game.Screens.Edit } // Todo: should probably be done at a DrawableRuleset level to share logic with Player. - clock = new EditorClock(playableBeatmap, beatDivisor) { IsCoupled = false }; + clock = new EditorClock(playableBeatmap, beatDivisor); clock.ChangeSource(loadableBeatmap.Track); dependencies.CacheAs(clock); diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index d55f2c0bf6..7a00a74530 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Edit this.beatDivisor = beatDivisor ?? new BindableBeatDivisor(); - underlyingClock = new FramedBeatmapClock(applyOffsets: true); + underlyingClock = new FramedBeatmapClock(applyOffsets: true) { IsCoupled = false }; AddInternal(underlyingClock); } @@ -222,21 +222,7 @@ namespace osu.Game.Screens.Edit public void ProcessFrame() { - // EditorClock wasn't being added in many places. This gives us more certainty that it is. - Debug.Assert(underlyingClock.LoadState > LoadState.NotLoaded); - - underlyingClock.ProcessFrame(); - - playbackFinished = CurrentTime >= TrackLength; - - if (playbackFinished) - { - if (IsRunning) - underlyingClock.Stop(); - - if (CurrentTime > TrackLength) - underlyingClock.Seek(TrackLength); - } + // Noop to ensure an external consumer doesn't process the internal clock an extra time. } public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime; @@ -253,18 +239,26 @@ namespace osu.Game.Screens.Edit public IClock Source => underlyingClock.Source; - public bool IsCoupled - { - get => underlyingClock.IsCoupled; - set => underlyingClock.IsCoupled = value; - } - private const double transform_time = 300; protected override void Update() { base.Update(); + // EditorClock wasn't being added in many places. This gives us more certainty that it is. + Debug.Assert(underlyingClock.LoadState > LoadState.NotLoaded); + + playbackFinished = CurrentTime >= TrackLength; + + if (playbackFinished) + { + if (IsRunning) + underlyingClock.Stop(); + + if (CurrentTime > TrackLength) + underlyingClock.Seek(TrackLength); + } + updateSeekingState(); } From 4b72e557701bf8358d771a710950d9126d593b93 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Aug 2022 15:40:01 +0900 Subject: [PATCH 378/532] Fix various test scenes not adding `EditorClock` to the draw hierarchy --- .../CatchSelectionBlueprintTestScene.cs | 9 +- .../Editor/TestSceneManiaHitObjectComposer.cs | 8 +- .../Editor/TestSceneOsuDistanceSnapGrid.cs | 3 +- .../Editor/TestSceneTaikoHitObjectComposer.cs | 2 +- ...tSceneHitObjectComposerDistanceSnapping.cs | 18 +- .../Visual/Editing/TestSceneEditorClock.cs | 48 +++--- .../Editing/TestSceneEditorSeekSnapping.cs | 159 +++++++++--------- .../Editing/TestSceneTapTimingControl.cs | 4 +- .../TestSceneTimelineBlueprintContainer.cs | 2 +- .../Visual/Editing/TestSceneTimingScreen.cs | 12 +- .../Visual/Editing/TimelineTestScene.cs | 2 +- osu.Game/Tests/Visual/EditorClockTestScene.cs | 44 ++--- .../Visual/PlacementBlueprintTestScene.cs | 11 +- .../Visual/SelectionBlueprintTestScene.cs | 17 +- 14 files changed, 174 insertions(+), 165 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs index ea17fa400c..60fb31d1e0 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs @@ -70,10 +70,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor [Cached] private readonly BindableBeatDivisor beatDivisor; + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + public EditorBeatmapDependencyContainer(IBeatmap beatmap, BindableBeatDivisor beatDivisor) { - editorClock = new EditorClock(beatmap, beatDivisor); this.beatDivisor = beatDivisor; + + InternalChildren = new Drawable[] + { + editorClock = new EditorClock(beatmap, beatDivisor), + Content, + }; } } } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index 4ad27348fc..fcc9e2e6c3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor public void Setup() => Schedule(() => { BeatDivisor.Value = 8; - Clock.Seek(0); + EditorClock.Seek(0); Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both }; }); @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); originalTime = lastObject.HitObject.StartTime; - Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); + EditorClock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); }); AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects)); @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); originalTime = lastObject.HitObject.StartTime; - Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); + EditorClock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); }); AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects)); @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor AddStep("seek to last object", () => { lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); - Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); + EditorClock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); }); AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects)); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index e54ce45ccc..c102678e00 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -59,10 +59,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } }); - editorClock = new EditorClock(editorBeatmap); - base.Content.Children = new Drawable[] { + editorClock = new EditorClock(editorBeatmap), snapProvider, Content }; diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs index 16f17f4131..8d17918a92 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor public void Setup() => Schedule(() => { BeatDivisor.Value = 8; - Clock.Seek(0); + EditorClock.Seek(0); Child = new TestComposer { RelativeSizeAxes = Axes.Both }; }); diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 0e80f8f699..1e87ed27df 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -23,13 +21,13 @@ namespace osu.Game.Tests.Editing [HeadlessTest] public class TestSceneHitObjectComposerDistanceSnapping : EditorClockTestScene { - private TestHitObjectComposer composer; + private TestHitObjectComposer composer = null!; [Cached(typeof(EditorBeatmap))] [Cached(typeof(IBeatSnapProvider))] private readonly EditorBeatmap editorBeatmap; - protected override Container Content { get; } + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; public TestSceneHitObjectComposerDistanceSnapping() { @@ -40,15 +38,9 @@ namespace osu.Game.Tests.Editing { editorBeatmap = new EditorBeatmap(new OsuBeatmap { - BeatmapInfo = - { - Ruleset = new OsuRuleset().RulesetInfo, - }, + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, }), - Content = new Container - { - RelativeSizeAxes = Axes.Both, - } + Content }, }); } @@ -205,7 +197,7 @@ namespace osu.Game.Tests.Editing assertSnappedDistance(400, 400); } - private void assertSnapDistance(float expectedDistance, HitObject hitObject = null) + private void assertSnapDistance(float expectedDistance, HitObject? hitObject = null) => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()), () => Is.EqualTo(expectedDistance)); private void assertDurationToDistance(double duration, float expectedDistance) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs index 3c6820e49b..d598ebafa9 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs @@ -55,51 +55,51 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestStopAtTrackEnd() { - AddStep("reset clock", () => Clock.Seek(0)); + AddStep("reset clock", () => EditorClock.Seek(0)); - AddStep("start clock", () => Clock.Start()); - AddAssert("clock running", () => Clock.IsRunning); + AddStep("start clock", () => EditorClock.Start()); + AddAssert("clock running", () => EditorClock.IsRunning); - AddStep("seek near end", () => Clock.Seek(Clock.TrackLength - 250)); - AddUntilStep("clock stops", () => !Clock.IsRunning); + AddStep("seek near end", () => EditorClock.Seek(EditorClock.TrackLength - 250)); + AddUntilStep("clock stops", () => !EditorClock.IsRunning); - AddUntilStep("clock stopped at end", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); + AddUntilStep("clock stopped at end", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength)); - AddStep("start clock again", () => Clock.Start()); - AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); + AddStep("start clock again", () => EditorClock.Start()); + AddAssert("clock looped to start", () => EditorClock.IsRunning && EditorClock.CurrentTime < 500); } [Test] public void TestWrapWhenStoppedAtTrackEnd() { - AddStep("reset clock", () => Clock.Seek(0)); + AddStep("reset clock", () => EditorClock.Seek(0)); - AddStep("stop clock", () => Clock.Stop()); - AddAssert("clock stopped", () => !Clock.IsRunning); + AddStep("stop clock", () => EditorClock.Stop()); + AddAssert("clock stopped", () => !EditorClock.IsRunning); - AddStep("seek exactly to end", () => Clock.Seek(Clock.TrackLength)); - AddAssert("clock stopped at end", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); + AddStep("seek exactly to end", () => EditorClock.Seek(EditorClock.TrackLength)); + AddAssert("clock stopped at end", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength)); - AddStep("start clock again", () => Clock.Start()); - AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); + AddStep("start clock again", () => EditorClock.Start()); + AddAssert("clock looped to start", () => EditorClock.IsRunning && EditorClock.CurrentTime < 500); } [Test] public void TestClampWhenSeekOutsideBeatmapBounds() { - AddStep("stop clock", () => Clock.Stop()); + AddStep("stop clock", () => EditorClock.Stop()); - AddStep("seek before start time", () => Clock.Seek(-1000)); - AddAssert("time is clamped to 0", () => Clock.CurrentTime, () => Is.EqualTo(0)); + AddStep("seek before start time", () => EditorClock.Seek(-1000)); + AddAssert("time is clamped to 0", () => EditorClock.CurrentTime, () => Is.EqualTo(0)); - AddStep("seek beyond track length", () => Clock.Seek(Clock.TrackLength + 1000)); - AddAssert("time is clamped to track length", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); + AddStep("seek beyond track length", () => EditorClock.Seek(EditorClock.TrackLength + 1000)); + AddAssert("time is clamped to track length", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength)); - AddStep("seek smoothly before start time", () => Clock.SeekSmoothlyTo(-1000)); - AddUntilStep("time is clamped to 0", () => Clock.CurrentTime, () => Is.EqualTo(0)); + AddStep("seek smoothly before start time", () => EditorClock.SeekSmoothlyTo(-1000)); + AddUntilStep("time is clamped to 0", () => EditorClock.CurrentTime, () => Is.EqualTo(0)); - AddStep("seek smoothly beyond track length", () => Clock.SeekSmoothlyTo(Clock.TrackLength + 1000)); - AddUntilStep("time is clamped to track length", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); + AddStep("seek smoothly beyond track length", () => EditorClock.SeekSmoothlyTo(EditorClock.TrackLength + 1000)); + AddUntilStep("time is clamped to track length", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength)); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs index 2465512dae..aa4bccd728 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs @@ -28,6 +28,11 @@ namespace osu.Game.Tests.Visual.Editing { base.LoadComplete(); + Child = new TimingPointVisualiser(Beatmap.Value.Beatmap, 5000) { Clock = EditorClock }; + } + + protected override Beatmap CreateEditorClockBeatmap() + { var testBeatmap = new Beatmap { ControlPointInfo = new ControlPointInfo(), @@ -45,9 +50,7 @@ namespace osu.Game.Tests.Visual.Editing testBeatmap.ControlPointInfo.Add(450, new TimingControlPoint { BeatLength = 100 }); testBeatmap.ControlPointInfo.Add(500, new TimingControlPoint { BeatLength = 307.69230769230802 }); - Beatmap.Value = CreateWorkingBeatmap(testBeatmap); - - Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock }; + return testBeatmap; } /// @@ -59,17 +62,17 @@ namespace osu.Game.Tests.Visual.Editing reset(); // Forwards - AddStep("Seek(0)", () => Clock.Seek(0)); + AddStep("Seek(0)", () => EditorClock.Seek(0)); checkTime(0); - AddStep("Seek(33)", () => Clock.Seek(33)); + AddStep("Seek(33)", () => EditorClock.Seek(33)); checkTime(33); - AddStep("Seek(89)", () => Clock.Seek(89)); + AddStep("Seek(89)", () => EditorClock.Seek(89)); checkTime(89); // Backwards - AddStep("Seek(25)", () => Clock.Seek(25)); + AddStep("Seek(25)", () => EditorClock.Seek(25)); checkTime(25); - AddStep("Seek(0)", () => Clock.Seek(0)); + AddStep("Seek(0)", () => EditorClock.Seek(0)); checkTime(0); } @@ -82,19 +85,19 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("Seek(0), Snap", () => Clock.SeekSnapped(0)); + AddStep("Seek(0), Snap", () => EditorClock.SeekSnapped(0)); checkTime(0); - AddStep("Seek(50), Snap", () => Clock.SeekSnapped(50)); + AddStep("Seek(50), Snap", () => EditorClock.SeekSnapped(50)); checkTime(50); - AddStep("Seek(100), Snap", () => Clock.SeekSnapped(100)); + AddStep("Seek(100), Snap", () => EditorClock.SeekSnapped(100)); checkTime(100); - AddStep("Seek(175), Snap", () => Clock.SeekSnapped(175)); + AddStep("Seek(175), Snap", () => EditorClock.SeekSnapped(175)); checkTime(175); - AddStep("Seek(350), Snap", () => Clock.SeekSnapped(350)); + AddStep("Seek(350), Snap", () => EditorClock.SeekSnapped(350)); checkTime(350); - AddStep("Seek(400), Snap", () => Clock.SeekSnapped(400)); + AddStep("Seek(400), Snap", () => EditorClock.SeekSnapped(400)); checkTime(400); - AddStep("Seek(450), Snap", () => Clock.SeekSnapped(450)); + AddStep("Seek(450), Snap", () => EditorClock.SeekSnapped(450)); checkTime(450); } @@ -107,17 +110,17 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("Seek(24), Snap", () => Clock.SeekSnapped(24)); + AddStep("Seek(24), Snap", () => EditorClock.SeekSnapped(24)); checkTime(0); - AddStep("Seek(26), Snap", () => Clock.SeekSnapped(26)); + AddStep("Seek(26), Snap", () => EditorClock.SeekSnapped(26)); checkTime(50); - AddStep("Seek(150), Snap", () => Clock.SeekSnapped(150)); + AddStep("Seek(150), Snap", () => EditorClock.SeekSnapped(150)); checkTime(100); - AddStep("Seek(170), Snap", () => Clock.SeekSnapped(170)); + AddStep("Seek(170), Snap", () => EditorClock.SeekSnapped(170)); checkTime(175); - AddStep("Seek(274), Snap", () => Clock.SeekSnapped(274)); + AddStep("Seek(274), Snap", () => EditorClock.SeekSnapped(274)); checkTime(175); - AddStep("Seek(276), Snap", () => Clock.SeekSnapped(276)); + AddStep("Seek(276), Snap", () => EditorClock.SeekSnapped(276)); checkTime(350); } @@ -129,15 +132,15 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("SeekForward", () => Clock.SeekForward()); + AddStep("SeekForward", () => EditorClock.SeekForward()); checkTime(50); - AddStep("SeekForward", () => Clock.SeekForward()); + AddStep("SeekForward", () => EditorClock.SeekForward()); checkTime(100); - AddStep("SeekForward", () => Clock.SeekForward()); + AddStep("SeekForward", () => EditorClock.SeekForward()); checkTime(200); - AddStep("SeekForward", () => Clock.SeekForward()); + AddStep("SeekForward", () => EditorClock.SeekForward()); checkTime(400); - AddStep("SeekForward", () => Clock.SeekForward()); + AddStep("SeekForward", () => EditorClock.SeekForward()); checkTime(450); } @@ -149,17 +152,17 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(50); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(100); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(175); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(350); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(400); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(450); } @@ -172,30 +175,30 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("Seek(49)", () => Clock.Seek(49)); + AddStep("Seek(49)", () => EditorClock.Seek(49)); checkTime(49); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(50); - AddStep("Seek(49.999)", () => Clock.Seek(49.999)); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("Seek(49.999)", () => EditorClock.Seek(49.999)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(100); - AddStep("Seek(99)", () => Clock.Seek(99)); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("Seek(99)", () => EditorClock.Seek(99)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(100); - AddStep("Seek(99.999)", () => Clock.Seek(99.999)); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("Seek(99.999)", () => EditorClock.Seek(99.999)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(150); - AddStep("Seek(174)", () => Clock.Seek(174)); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("Seek(174)", () => EditorClock.Seek(174)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(175); - AddStep("Seek(349)", () => Clock.Seek(349)); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("Seek(349)", () => EditorClock.Seek(349)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(350); - AddStep("Seek(399)", () => Clock.Seek(399)); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("Seek(399)", () => EditorClock.Seek(399)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(400); - AddStep("Seek(449)", () => Clock.Seek(449)); - AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddStep("Seek(449)", () => EditorClock.Seek(449)); + AddStep("SeekForward, Snap", () => EditorClock.SeekForward(true)); checkTime(450); } @@ -207,17 +210,17 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("Seek(450)", () => Clock.Seek(450)); + AddStep("Seek(450)", () => EditorClock.Seek(450)); checkTime(450); - AddStep("SeekBackward", () => Clock.SeekBackward()); + AddStep("SeekBackward", () => EditorClock.SeekBackward()); checkTime(400); - AddStep("SeekBackward", () => Clock.SeekBackward()); + AddStep("SeekBackward", () => EditorClock.SeekBackward()); checkTime(350); - AddStep("SeekBackward", () => Clock.SeekBackward()); + AddStep("SeekBackward", () => EditorClock.SeekBackward()); checkTime(150); - AddStep("SeekBackward", () => Clock.SeekBackward()); + AddStep("SeekBackward", () => EditorClock.SeekBackward()); checkTime(50); - AddStep("SeekBackward", () => Clock.SeekBackward()); + AddStep("SeekBackward", () => EditorClock.SeekBackward()); checkTime(0); } @@ -229,19 +232,19 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("Seek(450)", () => Clock.Seek(450)); + AddStep("Seek(450)", () => EditorClock.Seek(450)); checkTime(450); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(400); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(350); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(175); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(100); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(50); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(0); } @@ -254,18 +257,18 @@ namespace osu.Game.Tests.Visual.Editing { reset(); - AddStep("Seek(451)", () => Clock.Seek(451)); + AddStep("Seek(451)", () => EditorClock.Seek(451)); checkTime(451); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(450); - AddStep("Seek(450.999)", () => Clock.Seek(450.999)); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("Seek(450.999)", () => EditorClock.Seek(450.999)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(450); - AddStep("Seek(401)", () => Clock.Seek(401)); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("Seek(401)", () => EditorClock.Seek(401)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(400); - AddStep("Seek(401.999)", () => Clock.Seek(401.999)); - AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddStep("Seek(401.999)", () => EditorClock.Seek(401.999)); + AddStep("SeekBackward, Snap", () => EditorClock.SeekBackward(true)); checkTime(400); } @@ -279,37 +282,37 @@ namespace osu.Game.Tests.Visual.Editing double lastTime = 0; - AddStep("Seek(0)", () => Clock.Seek(0)); + AddStep("Seek(0)", () => EditorClock.Seek(0)); checkTime(0); for (int i = 0; i < 9; i++) { AddStep("SeekForward, Snap", () => { - lastTime = Clock.CurrentTime; - Clock.SeekForward(true); + lastTime = EditorClock.CurrentTime; + EditorClock.SeekForward(true); }); - AddAssert("Time > lastTime", () => Clock.CurrentTime > lastTime); + AddAssert("Time > lastTime", () => EditorClock.CurrentTime > lastTime); } for (int i = 0; i < 9; i++) { AddStep("SeekBackward, Snap", () => { - lastTime = Clock.CurrentTime; - Clock.SeekBackward(true); + lastTime = EditorClock.CurrentTime; + EditorClock.SeekBackward(true); }); - AddAssert("Time < lastTime", () => Clock.CurrentTime < lastTime); + AddAssert("Time < lastTime", () => EditorClock.CurrentTime < lastTime); } checkTime(0); } - private void checkTime(double expectedTime) => AddAssert($"Current time is {expectedTime}", () => Clock.CurrentTime, () => Is.EqualTo(expectedTime)); + private void checkTime(double expectedTime) => AddUntilStep($"Current time is {expectedTime}", () => EditorClock.CurrentTime, () => Is.EqualTo(expectedTime)); private void reset() { - AddStep("Reset", () => Clock.Seek(0)); + AddStep("Reset", () => EditorClock.Seek(0)); } private class TimingPointVisualiser : CompositeDrawable diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs index 7d881bc259..10e1206b53 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs @@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Editing .TriggerClick(); }); - AddUntilStep("wait for track playing", () => Clock.IsRunning); + AddUntilStep("wait for track playing", () => EditorClock.IsRunning); AddStep("click reset button", () => { @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Editing .TriggerClick(); }); - AddUntilStep("wait for track stopped", () => !Clock.IsRunning); + AddUntilStep("wait for track stopped", () => !EditorClock.IsRunning); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs index d2984728b0..c098b683a6 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.Editing protected override void LoadComplete() { base.LoadComplete(); - Clock.Seek(10000); + EditorClock.Seek(10000); } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index e262bd756a..03c184c27d 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Editing [SetUpSteps] public void SetUpSteps() { - AddStep("Stop clock", () => Clock.Stop()); + AddStep("Stop clock", () => EditorClock.Stop()); AddUntilStep("wait for rows to load", () => Child.ChildrenOfType().Any()); } @@ -68,10 +68,10 @@ namespace osu.Game.Tests.Visual.Editing }); AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670); - AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670); + AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 54670); - AddStep("Seek to just before next point", () => Clock.Seek(69000)); - AddStep("Start clock", () => Clock.Start()); + AddStep("Seek to just before next point", () => EditorClock.Seek(69000)); + AddStep("Start clock", () => EditorClock.Start()); AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670); } @@ -86,9 +86,9 @@ namespace osu.Game.Tests.Visual.Editing }); AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670); - AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670); + AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 54670); - AddStep("Seek to later", () => Clock.Seek(80000)); + AddStep("Seek to later", () => EditorClock.Seek(80000)); AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670); } diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 437f06c47f..d830f8d488 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Editing { base.LoadComplete(); - Clock.Seek(2500); + EditorClock.Seek(2500); } public abstract Drawable CreateTestComponent(); diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index 91284ae499..8f1e7abd9e 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -6,6 +6,8 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Overlays; @@ -24,30 +26,39 @@ namespace osu.Game.Tests.Visual protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor(); - [Cached] - protected new readonly EditorClock Clock; + protected EditorClock EditorClock; private readonly Bindable frequencyAdjustment = new BindableDouble(1); + private IBeatmap editorClockBeatmap; protected virtual bool ScrollUsingMouseWheel => true; - protected EditorClockTestScene() - { - Clock = new EditorClock(new Beatmap(), BeatDivisor) { IsCoupled = false }; - } + protected override Container Content => content; + + private readonly Container content = new Container { RelativeSizeAxes = Axes.Both }; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + editorClockBeatmap = CreateEditorClockBeatmap(); + + base.Content.AddRange(new Drawable[] + { + EditorClock = new EditorClock(editorClockBeatmap, BeatDivisor), + content + }); + dependencies.Cache(BeatDivisor); - dependencies.CacheAs(Clock); + dependencies.CacheAs(EditorClock); return dependencies; } protected override void LoadComplete() { + Beatmap.Value = CreateWorkingBeatmap(editorClockBeatmap); + base.LoadComplete(); Beatmap.BindValueChanged(beatmapChanged, true); @@ -55,22 +66,13 @@ namespace osu.Game.Tests.Visual AddSliderStep("editor clock rate", 0.0, 2.0, 1.0, v => frequencyAdjustment.Value = v); } + protected virtual IBeatmap CreateEditorClockBeatmap() => new Beatmap(); + private void beatmapChanged(ValueChangedEvent e) { e.OldValue?.Track.RemoveAdjustment(AdjustableProperty.Frequency, frequencyAdjustment); - - Clock.Beatmap = e.NewValue.Beatmap; - Clock.ChangeSource(e.NewValue.Track); - Clock.ProcessFrame(); - e.NewValue.Track.AddAdjustment(AdjustableProperty.Frequency, frequencyAdjustment); - } - - protected override void Update() - { - base.Update(); - - Clock.ProcessFrame(); + EditorClock.ChangeSource(e.NewValue.Track); } protected override bool OnScroll(ScrollEvent e) @@ -79,9 +81,9 @@ namespace osu.Game.Tests.Visual return false; if (e.ScrollDelta.Y > 0) - Clock.SeekBackward(true); + EditorClock.SeekBackward(true); else - Clock.SeekForward(true); + EditorClock.SeekForward(true); return true; } diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index 797e8363c3..176b181e73 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -30,10 +30,15 @@ namespace osu.Game.Tests.Visual { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(new EditorClock()); - var playable = GetPlayableBeatmap(); - dependencies.CacheAs(new EditorBeatmap(playable)); + + var editorClock = new EditorClock(); + base.Content.Add(editorClock); + dependencies.CacheAs(editorClock); + + var editorBeatmap = new EditorBeatmap(playable); + // Not adding to hierarchy as we don't satisfy its dependencies. Probably not good. + dependencies.CacheAs(editorBeatmap); return dependencies; } diff --git a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs index 66d79cad1c..ac0d1cd366 100644 --- a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -19,19 +16,23 @@ namespace osu.Game.Tests.Visual [Cached] private readonly EditorClock editorClock = new EditorClock(); - protected override Container Content => content ?? base.Content; + protected override Container Content => content; private readonly Container content; protected SelectionBlueprintTestScene() { - base.Content.Add(content = new Container + base.Content.AddRange(new Drawable[] { - Clock = new FramedClock(new StopwatchClock()), - RelativeSizeAxes = Axes.Both + editorClock, + content = new Container + { + Clock = new FramedClock(new StopwatchClock()), + RelativeSizeAxes = Axes.Both + } }); } - protected void AddBlueprint(HitObjectSelectionBlueprint blueprint, [CanBeNull] DrawableHitObject drawableObject = null) + protected void AddBlueprint(HitObjectSelectionBlueprint blueprint, DrawableHitObject? drawableObject = null) { Add(blueprint.With(d => { From cd90536e4bbb881b1787390d4469a57a5e6299d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 18:25:48 +0900 Subject: [PATCH 379/532] Remove `Track` access in `Timeline` --- .../Compose/Components/Timeline/Timeline.cs | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 54f2d13707..9e96a7386d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -64,8 +63,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// private bool trackWasPlaying; - private Track track; - /// /// The timeline zoom level at a 1x zoom scale. /// @@ -93,6 +90,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Bindable waveformOpacity; + private double trackLengthForZoom; + [BackgroundDependencyLoader] private void load(IBindable beatmap, OsuColour colours, OsuConfigManager config) { @@ -144,9 +143,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Beatmap.BindValueChanged(b => { waveform.Waveform = b.NewValue.Waveform; - track = b.NewValue.Track; - - setupTimelineZoom(); }, true); Zoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom); @@ -185,8 +181,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateWaveformOpacity() => waveform.FadeTo(WaveformVisible.Value ? waveformOpacity.Value : 0, 200, Easing.OutQuint); - private float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(track.Length / milliseconds)); - protected override void Update() { base.Update(); @@ -197,20 +191,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // This needs to happen after transforms are updated, but before the scroll position is updated in base.UpdateAfterChildren if (editorClock.IsRunning) scrollToTrackTime(); - } - private void setupTimelineZoom() - { - if (!track.IsLoaded) + if (editorClock.TrackLength != trackLengthForZoom) { - Scheduler.AddOnce(setupTimelineZoom); - return; + defaultTimelineZoom = getZoomLevelForVisibleMilliseconds(6000); + + float initialZoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom); + float minimumZoom = getZoomLevelForVisibleMilliseconds(10000); + float maximumZoom = getZoomLevelForVisibleMilliseconds(500); + + SetupZoom(initialZoom, minimumZoom, maximumZoom); + + float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(editorClock.TrackLength / milliseconds)); + + trackLengthForZoom = editorClock.TrackLength; } - - defaultTimelineZoom = getZoomLevelForVisibleMilliseconds(6000); - - float initialZoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom); - SetupZoom(initialZoom, getZoomLevelForVisibleMilliseconds(10000), getZoomLevelForVisibleMilliseconds(500)); } protected override bool OnScroll(ScrollEvent e) @@ -255,16 +250,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void seekTrackToCurrent() { - if (!track.IsLoaded) - return; - - double target = Current / Content.DrawWidth * track.Length; - editorClock.Seek(Math.Min(track.Length, target)); + double target = Current / Content.DrawWidth * editorClock.TrackLength; + editorClock.Seek(Math.Min(editorClock.TrackLength, target)); } private void scrollToTrackTime() { - if (!track.IsLoaded || track.Length == 0) + if (editorClock.TrackLength == 0) return; // covers the case where the user starts playback after a drag is in progress. @@ -272,7 +264,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (handlingDragInput) editorClock.Stop(); - ScrollTo((float)(editorClock.CurrentTime / track.Length) * Content.DrawWidth, false); + ScrollTo((float)(editorClock.CurrentTime / editorClock.TrackLength) * Content.DrawWidth, false); } protected override bool OnMouseDown(MouseDownEvent e) @@ -310,12 +302,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// /// The total amount of time visible on the timeline. /// - public double VisibleRange => track.Length / Zoom; + public double VisibleRange => editorClock.TrackLength / Zoom; public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) => new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); private double getTimeFromPosition(Vector2 localPosition) => - (localPosition.X / Content.DrawWidth) * track.Length; + (localPosition.X / Content.DrawWidth) * editorClock.TrackLength; } } From 9c9238d6e8686017d522752c3044156906393674 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 18:38:52 +0900 Subject: [PATCH 380/532] Fix `TimelineTestScene`'s beatmap getting overwritten by `EditorClockTestScene` --- .../Visual/Editing/TestSceneTimelineZoom.cs | 4 ---- .../Visual/Editing/TimelineTestScene.cs | 20 ++++++++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs index 630d048867..11ac102814 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs @@ -17,8 +17,6 @@ namespace osu.Game.Tests.Visual.Editing { double initialVisibleRange = 0; - AddUntilStep("wait for load", () => MusicController.TrackLoaded); - AddStep("reset zoom", () => TimelineArea.Timeline.Zoom = 100); AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange); @@ -36,8 +34,6 @@ namespace osu.Game.Tests.Visual.Editing { double initialVisibleRange = 0; - AddUntilStep("wait for load", () => MusicController.TrackLoaded); - AddStep("reset timeline size", () => TimelineArea.Timeline.Width = 1); AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange); diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index d830f8d488..19e297a08d 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -9,12 +9,14 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; +using osu.Game.Storyboards; using osuTK; using osuTK.Graphics; @@ -28,10 +30,14 @@ namespace osu.Game.Tests.Visual.Editing protected EditorBeatmap EditorBeatmap { get; private set; } - [BackgroundDependencyLoader] - private void load(AudioManager audio) + [Resolved] + private AudioManager audio { get; set; } + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new WaveformTestBeatmap(audio); + + protected override void LoadComplete() { - Beatmap.Value = new WaveformTestBeatmap(audio); + base.LoadComplete(); var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); EditorBeatmap = new EditorBeatmap(playable); @@ -68,11 +74,11 @@ namespace osu.Game.Tests.Visual.Editing }); } - protected override void LoadComplete() + [SetUpSteps] + public void SetUpSteps() { - base.LoadComplete(); - - EditorClock.Seek(2500); + AddUntilStep("wait for track loaded", () => MusicController.TrackLoaded); + AddStep("seek forward", () => EditorClock.Seek(2500)); } public abstract Drawable CreateTestComponent(); From f54047d17b0ae8d50546909e386aeec2d1c0ea1b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 18:55:20 +0900 Subject: [PATCH 381/532] Move selection clearing to top --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 14ed305b25..a955a1cce3 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -260,6 +260,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (toSplit.Count == 0) return; + editorBeatmap.SelectedHitObjects.Clear(); + foreach (var c in toSplit) { if (c == controlPoints[0] || c == controlPoints[^1] || c.Type is null) @@ -318,8 +320,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders foreach (var c in controlPoints) c.Position -= first; HitObject.Position += first; - - editorBeatmap.SelectedHitObjects.Clear(); } private void convertToStream() From 47cb163015e9ef4b46e05dbcfc49ec5bbd7bb53e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 19:09:03 +0900 Subject: [PATCH 382/532] Refactor splitting logic and comments slightly --- .../Components/PathControlPointVisualiser.cs | 6 ++--- .../Sliders/SliderSelectionBlueprint.cs | 25 ++++++++----------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 48e1d6405d..22cbab8938 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -107,14 +107,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private bool splitSelected() { - List toSplit = Pieces.Where(p => p.IsSelected.Value && isSplittable(p)).Select(p => p.ControlPoint).ToList(); + List controlPointsToSplitAt = Pieces.Where(p => p.IsSelected.Value && isSplittable(p)).Select(p => p.ControlPoint).ToList(); // Ensure that there are any points to be split - if (toSplit.Count == 0) + if (controlPointsToSplitAt.Count == 0) return false; changeHandler?.BeginChange(); - SplitControlPointsRequested?.Invoke(toSplit); + SplitControlPointsRequested?.Invoke(controlPointsToSplitAt); changeHandler?.EndChange(); // Since pieces are re-used, they will not point to the deleted control points while remaining selected diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index a955a1cce3..eb69efd636 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -251,34 +251,31 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HitObject.Position += first; } - private void splitControlPoints(List toSplit) + private void splitControlPoints(List controlPointsToSplitAt) { // Arbitrary gap in milliseconds to put between split slider pieces const double split_gap = 100; // Ensure that there are any points to be split - if (toSplit.Count == 0) + if (controlPointsToSplitAt.Count == 0) return; editorBeatmap.SelectedHitObjects.Clear(); - foreach (var c in toSplit) + foreach (var splitPoint in controlPointsToSplitAt) { - if (c == controlPoints[0] || c == controlPoints[^1] || c.Type is null) + if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type is null) continue; // Split off the section of slider before this control point so the remaining control points to split are in the latter part of the slider. - var splitControlPoints = controlPoints.TakeWhile(current => current != c).ToList(); + int index = controlPoints.IndexOf(splitPoint); - if (splitControlPoints.Count == 0) + if (index <= 0) continue; - foreach (var current in splitControlPoints) - { - controlPoints.Remove(current); - } - - splitControlPoints.Add(c); + // Extract the split portion and remove from the original slider. + var splitControlPoints = controlPoints.Take(index + 1).ToList(); + controlPoints.RemoveRange(0, index); // Turn the control points which were split off into a new slider. var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone(); @@ -314,8 +311,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - // The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position - // So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0) + // Once all required pieces have been split off, the original slider has the final split. + // As a final step, we must reset its control points to have an origin of (0,0). Vector2 first = controlPoints[0].Position; foreach (var c in controlPoints) c.Position -= first; From cb1c4a1bb106102e0756167ec877fb00d81cc04a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 19:16:12 +0900 Subject: [PATCH 383/532] Move sample checks to be inline in other existing tests --- .../Editor/TestSceneObjectMerging.cs | 53 +++---------------- 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 75a2361732..cdb2a7fe77 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -9,7 +9,6 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osuTK; using osuTK.Input; -using System.Collections.Generic; namespace osu.Game.Rulesets.Osu.Tests.Editor { @@ -78,6 +77,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor return sliderCreatedFor(args); }); + AddAssert("samples exist", sliderSampleExist); + AddStep("undo", () => Editor.Undo()); AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && slider is not null && objectsRestored(circle1, slider, circle2)); } @@ -123,6 +124,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor return sliderCreatedFor(args); }); + AddAssert("samples exist", sliderSampleExist); + AddAssert("merged slider matches first slider", () => { var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); @@ -166,50 +169,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor (pos: circle1.Position, pathType: PathType.Linear), (pos: circle2.Position, pathType: null))); + AddAssert("samples exist", sliderSampleExist); + AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner)); } - [Test] - public void TestSimpleMergeSample() - { - HitCircle? circle1 = null; - HitCircle? circle2 = null; - - - AddStep("select first two circles", () => - { - circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle); - circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1); - EditorClock.Seek(circle1.StartTime); - EditorBeatmap.SelectedHitObjects.Add(circle1); - EditorBeatmap.SelectedHitObjects.Add(circle2); - }); - - mergeSelection(); - - AddAssert("samples exist", () => sliderSampleExist()); - } - - [Test] - public void TestSliderCircleMergeSample() - { - Slider? slider = null; - HitCircle? circle = null; - - AddStep("select a slider followed by a circle", () => - { - slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider); - circle = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h.StartTime > slider.StartTime); - EditorClock.Seek(slider.StartTime); - EditorBeatmap.SelectedHitObjects.Add(slider); - EditorBeatmap.SelectedHitObjects.Add(circle); - }); - - mergeSelection(); - - AddAssert("samples exist", () => sliderSampleExist()); - } - private void mergeSelection() { AddStep("merge selection", () => @@ -259,10 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); - if (mergedSlider.Samples[0] is null) - return false; - - return true; + return mergedSlider.Samples[0] is not null; } } } From 08cb70b0931eac8edcfc3939c0341166dff54ea1 Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 26 Aug 2022 20:27:31 +1000 Subject: [PATCH 384/532] Lessen repeated angle nerf for objects further back in time --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 86b6170d13..a6e76db902 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators OsuDifficultyHitObject lastObj = osuCurrent; - int angleRepeatCount = 0; + double angleRepeatCount = 0.0; // We want to round angles to make abusing the nerf a bit harder. double initialRoundedAngle = 0.0; @@ -82,8 +82,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { double roundedAngle = Math.Round(MathUtils.RadiansToDegrees(currentObj.Angle.Value) / 2.0) * 2.0; + // Objects further back in time should count less for the nerf. if (roundedAngle == initialRoundedAngle) - angleRepeatCount++; + angleRepeatCount += 1.0 - 0.1 * i; } } From 5082ee26cfeb62b7ce8560f9253e92f8434f606b Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 26 Aug 2022 20:30:14 +1000 Subject: [PATCH 385/532] Ensure a negative value cannot be added to angleRepeatCount --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index a6e76db902..2733217b64 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Objects further back in time should count less for the nerf. if (roundedAngle == initialRoundedAngle) - angleRepeatCount += 1.0 - 0.1 * i; + angleRepeatCount += Math.Max(1.0 - 0.1 * i, 0.0); } } From 249c3f868f6c426985a3631d2ad4f3773bbffb7d Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 26 Aug 2022 20:40:18 +1000 Subject: [PATCH 386/532] Compare raw angle values instead of rounding angles --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 2733217b64..9630da5a01 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -49,11 +49,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double angleRepeatCount = 0.0; - // We want to round angles to make abusing the nerf a bit harder. - double initialRoundedAngle = 0.0; - if (osuCurrent.Angle != null) - initialRoundedAngle = Math.Round(MathUtils.RadiansToDegrees(osuCurrent.Angle.Value) / 2.0) * 2.0; - // This is iterating backwards in time from the current object. for (int i = 0; i < Math.Min(current.Index, 10); i++) { @@ -80,10 +75,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (currentObj.Angle != null && osuCurrent.Angle != null) { - double roundedAngle = Math.Round(MathUtils.RadiansToDegrees(currentObj.Angle.Value) / 2.0) * 2.0; - // Objects further back in time should count less for the nerf. - if (roundedAngle == initialRoundedAngle) + if (Math.Abs(currentObj.Angle.Value - osuCurrent.Angle.Value) < 0.02) angleRepeatCount += Math.Max(1.0 - 0.1 * i, 0.0); } } From 454d868dd598148f7ea141f83de2e9dbdb73a97c Mon Sep 17 00:00:00 2001 From: MBmasher Date: Fri, 26 Aug 2022 20:42:02 +1000 Subject: [PATCH 387/532] Remove unnecessary using call --- .../Difficulty/Evaluators/FlashlightEvaluator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 9630da5a01..2ba856d014 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -7,7 +7,6 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; -using osu.Framework.Utils; namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { From 5ef8e26ebe894322c8aaba026051c3bba5ab15f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 19:59:25 +0900 Subject: [PATCH 388/532] Fix check not accounting for mods not existing in certain rulesets Also check all instances, rather than first. --- .../Visual/Gameplay/TestSceneModValidity.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs index aa8dfaaa7e..0c6b656ab6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneModValidity.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -30,17 +29,19 @@ namespace osu.Game.Tests.Visual.Gameplay IEnumerable modInstances = mods.Select(mod => mod.CreateInstance()); - foreach (var mod in modInstances) + foreach (var modToCheck in modInstances) { - var modIncompatibilities = mod.IncompatibleMods; + var incompatibleMods = modToCheck.IncompatibleMods; - foreach (var incompatibleModType in modIncompatibilities) + foreach (var incompatible in incompatibleMods) { - var incompatibleMod = modInstances.First(m => incompatibleModType.IsInstanceOfType(m)); - Assert.That( - incompatibleMod.IncompatibleMods.Any(m => m.IsInstanceOfType(mod)), - $"{mod} has {incompatibleMod} in it's incompatible mods, but {incompatibleMod} does not have {mod} in it's incompatible mods." - ); + foreach (var incompatibleMod in modInstances.Where(m => incompatible.IsInstanceOfType(m))) + { + Assert.That( + incompatibleMod.IncompatibleMods.Any(m => m.IsInstanceOfType(modToCheck)), + $"{modToCheck} has {incompatibleMod} in it's incompatible mods, but {incompatibleMod} does not have {modToCheck} in it's incompatible mods." + ); + } } } }); From 81c0a641b4f596ca4c9b0a7d9c19b97c389f2ee1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 26 Aug 2022 14:51:08 +0300 Subject: [PATCH 389/532] Fix selection fallback path not updated to check inserted indices --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index e9f676d32f..0cbc17c67a 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -299,7 +299,7 @@ namespace osu.Game.Screens.Select // If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed. // Let's attempt to follow set-level selection anyway. - SelectBeatmap(sender[changes.NewModifiedIndices.First()].Beatmaps.First()); + SelectBeatmap(sender[modifiedAndInserted.First()].Beatmaps.First()); } } } From a3e595a9aac215987a6cb5ee5cad82cfad615a5c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 26 Aug 2022 14:51:19 +0300 Subject: [PATCH 390/532] Update comment to include inserted indices --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 0cbc17c67a..0f130714f1 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -277,7 +277,7 @@ namespace osu.Game.Screens.Select if (selectedSetMarkedDeleted && modifiedAndInserted.Any()) { - // If it is no longer valid, make the bold assumption that an updated version will be available in the modified indices. + // If it is no longer valid, make the bold assumption that an updated version will be available in the modified/inserted indices. // This relies on the full update operation being in a single transaction, so please don't change that. foreach (int i in modifiedAndInserted) { From 470bec79490b28e8eabe10f1f706d2806ddfaa64 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 23:29:03 +0900 Subject: [PATCH 391/532] Move private method down --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 60c8dcef21..1984840553 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -170,41 +170,6 @@ namespace osu.Game.Graphics.UserInterface selectionStarted = false; } - private void playSelectSample(SelectionSampleType selectionType) - { - if (Time.Current < sampleLastPlaybackTime + 15) return; - - SampleChannel? channel; - double pitch = 0.98 + RNG.NextDouble(0.04); - - switch (selectionType) - { - case SelectionSampleType.All: - channel = selectAllSample?.GetChannel(); - break; - - case SelectionSampleType.Word: - channel = selectWordSample?.GetChannel(); - break; - - case SelectionSampleType.Deselect: - channel = deselectSample?.GetChannel(); - break; - - default: - channel = selectCharSample?.GetChannel(); - pitch += (SelectedText.Length / (double)Text.Length) * 0.15f; - break; - } - - if (channel == null) return; - - channel.Frequency.Value = pitch; - channel.Play(); - - sampleLastPlaybackTime = Time.Current; - } - protected override void OnImeComposition(string newComposition, int removedTextLength, int addedTextLength, bool caretMoved) { base.OnImeComposition(newComposition, removedTextLength, addedTextLength, caretMoved); @@ -294,6 +259,41 @@ namespace osu.Game.Graphics.UserInterface SelectionColour = SelectionColour, }; + private void playSelectSample(SelectionSampleType selectionType) + { + if (Time.Current < sampleLastPlaybackTime + 15) return; + + SampleChannel? channel; + double pitch = 0.98 + RNG.NextDouble(0.04); + + switch (selectionType) + { + case SelectionSampleType.All: + channel = selectAllSample?.GetChannel(); + break; + + case SelectionSampleType.Word: + channel = selectWordSample?.GetChannel(); + break; + + case SelectionSampleType.Deselect: + channel = deselectSample?.GetChannel(); + break; + + default: + channel = selectCharSample?.GetChannel(); + pitch += (SelectedText.Length / (double)Text.Length) * 0.15f; + break; + } + + if (channel == null) return; + + channel.Frequency.Value = pitch; + channel.Play(); + + sampleLastPlaybackTime = Time.Current; + } + private void playTextAddedSample() => textAddedSamples[RNG.Next(0, textAddedSamples.Length)]?.Play(); private class OsuCaret : Caret From b082dc1fe47f30662d7f2f81a3ac5766371a1bf9 Mon Sep 17 00:00:00 2001 From: MBmasher Date: Sat, 27 Aug 2022 18:31:07 +1000 Subject: [PATCH 392/532] Slightly buff flashlight multiplier --- osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 84ef109598..40448c444c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills hasHiddenMod = mods.Any(m => m is OsuModHidden); } - private double skillMultiplier => 0.05; + private double skillMultiplier => 0.052; private double strainDecayBase => 0.15; private double currentStrain; From 16e0ec2f88f7924c4a9f965a735c5a635981c300 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 27 Aug 2022 13:53:50 +0200 Subject: [PATCH 393/532] Fixed 0 length merge being allowed --- .../Editor/TestSceneObjectMerging.cs | 43 +++++++++++++++++++ .../Edit/OsuSelectionHandler.cs | 7 ++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index cdb2a7fe77..4673c3d1d9 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -3,9 +3,11 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; using osuTK; using osuTK.Input; @@ -14,6 +16,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneObjectMerging : TestSceneOsuEditor { + private OsuSelectionHandler selectionHandler => Editor.ChildrenOfType().First(); + [Test] public void TestSimpleMerge() { @@ -29,6 +33,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor EditorBeatmap.SelectedHitObjects.Add(circle2); }); + moveMouseToHitObject(1); + AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection")); + mergeSelection(); AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( @@ -174,6 +181,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner)); } + [Test] + public void TestIllegalMerge() + { + HitCircle? circle1 = null; + HitCircle? circle2 = null; + + AddStep("add two circles on the same position", () => + { + circle1 = new HitCircle(); + circle2 = new HitCircle { Position = circle1.Position + Vector2.UnitX, StartTime = 1 }; + EditorClock.Seek(0); + EditorBeatmap.Add(circle1); + EditorBeatmap.Add(circle2); + EditorBeatmap.SelectedHitObjects.Add(circle1); + EditorBeatmap.SelectedHitObjects.Add(circle2); + }); + + moveMouseToHitObject(1); + AddAssert("merge option not available", () => selectionHandler.ContextMenuItems.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection")); + mergeSelection(); + AddAssert("circles not merged", () => circle1 is not null && circle2 is not null + && EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2)); + } + private void mergeSelection() { AddStep("merge selection", () => @@ -225,5 +256,17 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor return mergedSlider.Samples[0] is not null; } + + private void moveMouseToHitObject(int index) + { + AddStep($"hover mouse over hit object {index}", () => + { + if (EditorBeatmap.HitObjects.Count <= index) + return; + + Vector2 position = ((OsuHitObject)EditorBeatmap.HitObjects[index]).Position; + InputManager.MoveMouseTo(selectionHandler.ToScreenSpace(position)); + }); + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 2c5bbdb279..8b67c0dcc9 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -358,7 +358,8 @@ namespace osu.Game.Rulesets.Osu.Edit { var mergeableObjects = selectedMergeableObjects; - if (mergeableObjects.Length < 2) + if (mergeableObjects.Length < 2 || (mergeableObjects.All(h => h is not Slider) + && Precision.AlmostBigger(1, Vector2.DistanceSquared(mergeableObjects[0].Position, mergeableObjects[1].Position)))) return; ChangeHandler?.BeginChange(); @@ -445,7 +446,9 @@ namespace osu.Game.Rulesets.Osu.Edit foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; - if (selectedMergeableObjects.Length > 1) + var mergeableObjects = selectedMergeableObjects; + if (mergeableObjects.Length > 1 && (mergeableObjects.Any(h => h is Slider) + || Precision.DefinitelyBigger(Vector2.DistanceSquared(mergeableObjects[0].Position, mergeableObjects[1].Position), 1))) yield return new OsuMenuItem("Merge selection", MenuItemType.Destructive, mergeSelection); } } From ff2eac79d14934b5d6e179d38792d196484b77bd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 27 Aug 2022 17:43:32 +0200 Subject: [PATCH 394/532] fix same time merge causing crash --- .../Editor/TestSceneObjectMerging.cs | 29 ++++++++++++++++++- .../Edit/OsuSelectionHandler.cs | 2 ++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 4673c3d1d9..5c5384e0b7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("add two circles on the same position", () => { circle1 = new HitCircle(); - circle2 = new HitCircle { Position = circle1.Position + Vector2.UnitX, StartTime = 1 }; + circle2 = new HitCircle { Position = circle1.Position + Vector2.UnitX }; EditorClock.Seek(0); EditorBeatmap.Add(circle1); EditorBeatmap.Add(circle2); @@ -205,6 +205,33 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor && EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2)); } + [Test] + public void TestSameStartTimeMerge() + { + HitCircle? circle1 = null; + HitCircle? circle2 = null; + + AddStep("add two circles at the same time", () => + { + circle1 = new HitCircle(); + circle2 = new HitCircle { Position = circle1.Position + 100 * Vector2.UnitX }; + EditorClock.Seek(0); + EditorBeatmap.Add(circle1); + EditorBeatmap.Add(circle2); + EditorBeatmap.SelectedHitObjects.Add(circle1); + EditorBeatmap.SelectedHitObjects.Add(circle2); + }); + + moveMouseToHitObject(1); + AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection")); + + mergeSelection(); + + AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( + (pos: circle1.Position, pathType: PathType.Linear), + (pos: circle2.Position, pathType: null))); + } + private void mergeSelection() { AddStep("merge selection", () => diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 8b67c0dcc9..3d425afe9e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -363,6 +363,7 @@ namespace osu.Game.Rulesets.Osu.Edit return; ChangeHandler?.BeginChange(); + EditorBeatmap.BeginChange(); // Have an initial slider object. var firstHitObject = mergeableObjects[0]; @@ -438,6 +439,7 @@ namespace osu.Game.Rulesets.Osu.Edit SelectedItems.Clear(); SelectedItems.Add(mergedHitObject); + EditorBeatmap.EndChange(); ChangeHandler?.EndChange(); } From 0cc6a76c17f6f5e9c0b36a747bfa349369f4e82c Mon Sep 17 00:00:00 2001 From: its5Q Date: Sun, 28 Aug 2022 14:13:38 +1000 Subject: [PATCH 395/532] Fix crash with legacy import from incomplete installs --- osu.Game/Database/LegacyBeatmapImporter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Database/LegacyBeatmapImporter.cs b/osu.Game/Database/LegacyBeatmapImporter.cs index 6805cb36b8..0955461609 100644 --- a/osu.Game/Database/LegacyBeatmapImporter.cs +++ b/osu.Game/Database/LegacyBeatmapImporter.cs @@ -20,6 +20,10 @@ namespace osu.Game.Database protected override IEnumerable GetStableImportPaths(Storage storage) { + // make sure the directory exists + if (!storage.ExistsDirectory(string.Empty)) + yield break; + foreach (string directory in storage.GetDirectories(string.Empty)) { var directoryStorage = storage.GetStorageForDirectory(directory); From c0b13c7e1fa33da7681957dd62326632cba564e1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Aug 2022 19:39:12 +0900 Subject: [PATCH 396/532] Refactor ScoreProcessor ComputeScore() methods --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 4 +- .../PerformanceBreakdownCalculator.cs | 2 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 60 +------------------ osu.Game/Scoring/ScoreManager.cs | 2 +- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 2 +- 5 files changed, 8 insertions(+), 62 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 44ebdad2e4..7a45d1e7cf 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -307,7 +307,7 @@ namespace osu.Game.Tests.Rulesets.Scoring HitObjects = { new TestHitObject(result) } }); - Assert.That(scoreProcessor.ComputeFinalScore(ScoringMode.Standardised, new ScoreInfo + Assert.That(scoreProcessor.ComputeScore(ScoringMode.Standardised, new ScoreInfo { Ruleset = new TestRuleset().RulesetInfo, MaxCombo = result.AffectsCombo() ? 1 : 0, @@ -350,7 +350,7 @@ namespace osu.Game.Tests.Rulesets.Scoring } }; - double totalScore = new TestScoreProcessor().ComputeFinalScore(ScoringMode.Standardised, testScore); + double totalScore = new TestScoreProcessor().ComputeScore(ScoringMode.Standardised, testScore); Assert.That(totalScore, Is.EqualTo(750_000)); // 500K from accuracy (100%), and 250K from combo (50%). } #pragma warning restore CS0618 diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs index 4465f1a328..3fb12041d1 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Difficulty // calculate total score ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = perfectPlay.Mods; - perfectPlay.TotalScore = (long)scoreProcessor.ComputeFinalScore(ScoringMode.Standardised, perfectPlay); + perfectPlay.TotalScore = (long)scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay); // compute rank achieved // default to SS, then adjust the rank with mods diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 5b21caee84..f13e3e6de6 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Scoring /// /// The maximum of a basic (non-tick and non-bonus) hitobject. - /// Only populated via or . + /// Only populated via or . /// private HitResult? maxBasicResult; @@ -281,7 +281,7 @@ namespace osu.Game.Rulesets.Scoring /// The to compute the total score of. /// The total score in the given . [Pure] - public double ComputeFinalScore(ScoringMode mode, ScoreInfo scoreInfo) + public double ComputeScore(ScoringMode mode, ScoreInfo scoreInfo) { if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); @@ -291,60 +291,6 @@ namespace osu.Game.Rulesets.Scoring return ComputeScore(mode, current, maximum); } - /// - /// Computes the total score of a partially-completed . This should be used when it is unknown whether a score is complete. - /// - /// - /// Requires to have been called before use. - /// - /// The to represent the score as. - /// The to compute the total score of. - /// The total score in the given . - [Pure] - public double ComputePartialScore(ScoringMode mode, ScoreInfo scoreInfo) - { - if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) - throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); - - if (!beatmapApplied) - throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}."); - - ExtractScoringValues(scoreInfo, out var current, out _); - - return ComputeScore(mode, current, MaximumScoringValues); - } - - /// - /// Computes the total score of a given with a given custom max achievable combo. - /// - /// - /// This is useful for processing legacy scores in which the maximum achievable combo can be more accurately determined via external means (e.g. database values or difficulty calculation). - ///

Does not require to have been called before use.

- ///
- /// The to represent the score as. - /// The to compute the total score of. - /// The maximum achievable combo for the provided beatmap. - /// The total score in the given . - [Pure] - public double ComputeFinalLegacyScore(ScoringMode mode, ScoreInfo scoreInfo, int maxAchievableCombo) - { - if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) - throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); - - double accuracyRatio = scoreInfo.Accuracy; - double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1; - - ExtractScoringValues(scoreInfo, out var current, out var maximum); - - // For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score. - // To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score. - // Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together. - if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3 && maximum.BaseScore > 0) - accuracyRatio = current.BaseScore / maximum.BaseScore; - - return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects); - } - /// /// Computes the total score from scoring values. /// @@ -454,7 +400,7 @@ namespace osu.Game.Rulesets.Scoring score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result); // Populate total score after everything else. - score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score)); + score.TotalScore = (long)Math.Round(ComputeScore(ScoringMode.Standardised, score)); } /// diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 7367a1ef77..ecd37c761c 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -148,7 +148,7 @@ namespace osu.Game.Scoring var scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = score.Mods; - return (long)Math.Round(scoreProcessor.ComputeFinalLegacyScore(mode, score, beatmapMaxCombo.Value)); + return (long)Math.Round(scoreProcessor.ComputeScore(mode, score)); } /// diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 282c1db585..1c4d02bb11 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); - Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.ComputeFinalScore(ScoringMode.Standardised, Score.ScoreInfo)); + Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo)); } protected override void Dispose(bool isDisposing) From 90b9c02ac64ca410fd8440d8d30c4a607aaeffeb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 14:01:04 +0900 Subject: [PATCH 397/532] Remove `"internal"` identifier as unnecessary --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 30 +++++++++---------- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 20 ++++++------- osu.Game.Rulesets.Osu/OsuRuleset.cs | 40 ++++++++++++------------- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 32 ++++++++++---------- osu.Game/Rulesets/RealmRulesetStore.cs | 2 -- osu.Game/Rulesets/Ruleset.cs | 37 ++++++++++++----------- 6 files changed, 80 insertions(+), 81 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index f94bf276a0..321399c597 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -3,30 +3,30 @@ #nullable disable -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Rulesets.Catch.Mods; -using osu.Game.Rulesets.Catch.UI; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.UI; +using System; using System.Collections.Generic; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; -using osu.Game.Rulesets.Catch.Replays; -using osu.Game.Rulesets.Replays.Types; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; +using osu.Game.Graphics; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty; -using osu.Game.Rulesets.Catch.Scoring; -using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Scoring; -using System; -using osu.Framework.Extensions.EnumExtensions; -using osu.Framework.Localisation; using osu.Game.Rulesets.Catch.Edit; +using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Catch.Replays; +using osu.Game.Rulesets.Catch.Scoring; using osu.Game.Rulesets.Catch.Skinning.Legacy; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays.Types; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch public const string SHORT_NAME = "fruits"; - public override string RulesetAPIVersionSupported => "internal"; + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index b1fe4b30c4..813e2c461a 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -4,11 +4,6 @@ #nullable disable using System; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mania.Mods; -using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.UI; using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.EnumExtensions; @@ -16,11 +11,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Localisation; -using osu.Game.Graphics; -using osu.Game.Rulesets.Mania.Replays; -using osu.Game.Rulesets.Replays.Types; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; @@ -31,13 +25,19 @@ using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Difficulty; using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.Edit.Setup; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Skinning.Legacy; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; -using osu.Game.Skinning; +using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Ranking.Statistics; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania { @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania public const string SHORT_NAME = "mania"; - public override string RulesetAPIVersionSupported => "internal"; + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this); diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 4400dfbb65..226299d168 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -3,42 +3,42 @@ #nullable disable -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.UI; +using System; using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; -using osu.Game.Overlays.Settings; using osu.Framework.Input.Bindings; -using osu.Game.Rulesets.Osu.Edit; -using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Osu.Replays; -using osu.Game.Rulesets.Replays.Types; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Difficulty; -using osu.Game.Rulesets.Osu.Scoring; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Skinning; -using System; -using System.Linq; -using osu.Framework.Extensions.EnumExtensions; -using osu.Framework.Localisation; +using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit.Setup; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.Statistics; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Replays.Types; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Ranking.Statistics; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu { @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu public const string SHORT_NAME = "osu"; - public override string RulesetAPIVersionSupported => "internal"; + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 275c7144a7..f4eb1c68b3 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -3,33 +3,33 @@ #nullable disable -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Taiko.Mods; -using osu.Game.Rulesets.Taiko.UI; -using osu.Game.Rulesets.UI; +using System; using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; -using osu.Game.Rulesets.Replays.Types; -using osu.Game.Rulesets.Taiko.Replays; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; +using osu.Game.Graphics; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Difficulty; -using osu.Game.Rulesets.Taiko.Scoring; -using osu.Game.Scoring; -using System; -using System.Linq; -using osu.Framework.Extensions.EnumExtensions; -using osu.Framework.Localisation; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Edit; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Rulesets.Taiko.Skinning.Legacy; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Taiko public const string SHORT_NAME = "taiko"; - public override string RulesetAPIVersionSupported => "internal"; + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { diff --git a/osu.Game/Rulesets/RealmRulesetStore.cs b/osu.Game/Rulesets/RealmRulesetStore.cs index 590f118b68..456f6e399b 100644 --- a/osu.Game/Rulesets/RealmRulesetStore.cs +++ b/osu.Game/Rulesets/RealmRulesetStore.cs @@ -119,8 +119,6 @@ namespace osu.Game.Rulesets // Consider rulesets which haven't override the version as up-to-date for now. // At some point (once ruleset devs add versioning), we'll probably want to disallow this for deployed builds. case @"": - // Rulesets which are bundled with the game. Saves having to update their versions each bump. - case @"internal": // Ruleset is up-to-date, all good. case Ruleset.CURRENT_RULESET_API_VERSION: return true; diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 63f5906f46..cb72a1f20f 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -7,33 +7,33 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.IO.Stores; -using osu.Game.Beatmaps; -using osu.Game.Overlays.Settings; -using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Replays.Types; -using osu.Game.Rulesets.UI; -using osu.Game.Beatmaps.Legacy; -using osu.Game.Configuration; -using osu.Game.Rulesets.Configuration; -using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Skinning; -using osu.Game.Users; -using JetBrains.Annotations; -using osu.Framework.Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Localisation; using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Legacy; +using osu.Game.Configuration; using osu.Game.Extensions; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Configuration; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Filter; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays.Types; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Ranking.Statistics; +using osu.Game.Skinning; +using osu.Game.Users; namespace osu.Game.Rulesets { @@ -56,7 +56,8 @@ namespace osu.Game.Rulesets /// Ruleset implementations should be updated to support the latest version to ensure they can still be loaded. /// /// - /// When updating a ruleset to support the latest API, you should set this to . + /// Generally, all ruleset implementations should point this directly to . + /// This will ensure that each time you compile a new release, it will pull in the most recent version. /// See https://github.com/ppy/osu/wiki/Breaking-Changes for full details on required ongoing changes. /// public virtual string RulesetAPIVersionSupported => string.Empty; From 5ff4e6a4fe657b6c2f420bc1df3eddb028401638 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 14:26:09 +0900 Subject: [PATCH 398/532] Add test coverage for outdated ruleset --- osu.Game.Tests/Database/RulesetStoreTests.cs | 87 +++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index 795e90f543..dedec6dc83 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -1,11 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Difficulty; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.UI; namespace osu.Game.Tests.Database { @@ -51,5 +59,80 @@ namespace osu.Game.Tests.Database Assert.IsFalse(rulesets.GetRuleset("mania")?.IsManaged); }); } + + [Test] + public void TestOutdatedRulesetNotAvailable() + { + RunTestWithRealm((realm, storage) => + { + OutdatedRuleset.Version = "2021.101.0"; + OutdatedRuleset.HasImplementations = true; + + var ruleset = new OutdatedRuleset(); + string rulesetShortName = ruleset.RulesetInfo.ShortName; + + realm.Write(r => r.Add(new RulesetInfo(rulesetShortName, ruleset.RulesetInfo.Name, ruleset.RulesetInfo.InstantiationInfo, ruleset.RulesetInfo.OnlineID) + { + Available = true, + })); + + Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + + // Availability is updated on construction of a RealmRulesetStore + var _ = new RealmRulesetStore(realm, storage); + + Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.False); + + // Simulate the ruleset getting updated + OutdatedRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; + var __ = new RealmRulesetStore(realm, storage); + + Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + }); + } + + private class OutdatedRuleset : Ruleset + { + public override string RulesetAPIVersionSupported => Version; + + public static bool HasImplementations = true; + + public static string Version { get; set; } = CURRENT_RULESET_API_VERSION; + + public override IEnumerable GetModsFor(ModType type) + { + if (!HasImplementations) + throw new NotImplementedException(); + + return Array.Empty(); + } + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) + { + if (!HasImplementations) + throw new NotImplementedException(); + + return new DrawableOsuRuleset(new OsuRuleset(), beatmap, mods); + } + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) + { + if (!HasImplementations) + throw new NotImplementedException(); + + return new OsuBeatmapConverter(beatmap, new OsuRuleset()); + } + + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) + { + if (!HasImplementations) + throw new NotImplementedException(); + + return new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap); + } + + public override string Description => "outdated ruleset"; + public override string ShortName => "ruleset-outdated"; + } } } From 892f43da433a96d408f8b8d723d03c677d1d3653 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 14:28:55 +0900 Subject: [PATCH 399/532] Add test coverage of ruleset being marked unavailable if methods are throwing --- osu.Game.Tests/Database/RulesetStoreTests.cs | 35 +++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index dedec6dc83..a5662fa121 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -60,15 +60,40 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestRulesetThrowingOnMethods() + { + RunTestWithRealm((realm, storage) => + { + LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; + LoadTestRuleset.HasImplementations = false; + + var ruleset = new LoadTestRuleset(); + string rulesetShortName = ruleset.RulesetInfo.ShortName; + + realm.Write(r => r.Add(new RulesetInfo(rulesetShortName, ruleset.RulesetInfo.Name, ruleset.RulesetInfo.InstantiationInfo, ruleset.RulesetInfo.OnlineID) + { + Available = true, + })); + + Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); + + // Availability is updated on construction of a RealmRulesetStore + var _ = new RealmRulesetStore(realm, storage); + + Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.False); + }); + } + [Test] public void TestOutdatedRulesetNotAvailable() { RunTestWithRealm((realm, storage) => { - OutdatedRuleset.Version = "2021.101.0"; - OutdatedRuleset.HasImplementations = true; + LoadTestRuleset.Version = "2021.101.0"; + LoadTestRuleset.HasImplementations = true; - var ruleset = new OutdatedRuleset(); + var ruleset = new LoadTestRuleset(); string rulesetShortName = ruleset.RulesetInfo.ShortName; realm.Write(r => r.Add(new RulesetInfo(rulesetShortName, ruleset.RulesetInfo.Name, ruleset.RulesetInfo.InstantiationInfo, ruleset.RulesetInfo.OnlineID) @@ -84,14 +109,14 @@ namespace osu.Game.Tests.Database Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.False); // Simulate the ruleset getting updated - OutdatedRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; + LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION; var __ = new RealmRulesetStore(realm, storage); Assert.That(realm.Run(r => r.Find(rulesetShortName).Available), Is.True); }); } - private class OutdatedRuleset : Ruleset + private class LoadTestRuleset : Ruleset { public override string RulesetAPIVersionSupported => Version; From e8ae6840ea6da67cb206e55955c1040951378769 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 15:23:34 +0900 Subject: [PATCH 400/532] Add test coverage of selection being retained --- .../SongSelect/TestScenePlaySongSelect.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 3d8f496c9a..5db46e3097 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -6,15 +6,18 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; @@ -24,6 +27,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -413,6 +417,55 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); } + [Test] + public void TestSelectionRetainedOnBeatmapUpdate() + { + createSongSelect(); + changeRuleset(0); + + Live original = null!; + int originalOnlineSetID = 0; + + AddStep(@"Sort by artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist)); + + AddStep("import original", () => + { + original = manager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely(); + originalOnlineSetID = original!.Value.OnlineID; + }); + + // This will move the beatmap set to a different location in the carousel. + AddStep("Update original with bogus info", () => + { + original.PerformWrite(set => + { + foreach (var beatmap in set.Beatmaps) + { + beatmap.Metadata.Artist = "ZZZZZ"; + beatmap.OnlineID = 12804; + } + }); + }); + + AddRepeatStep("import other beatmaps", () => + { + var testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(); + + foreach (var beatmap in testBeatmapSetInfo.Beatmaps) + beatmap.Metadata.Artist = ((char)RNG.Next('A', 'Z')).ToString(); + + manager.Import(testBeatmapSetInfo); + }, 10); + + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID); + + Task> updateTask = null!; + AddStep("update beatmap", () => updateTask = manager.ImportAsUpdate(new ProgressNotification(), new ImportTask(TestResources.GetQuickTestBeatmapForImport()), original.Value)); + AddUntilStep("wait for update completion", () => updateTask.IsCompleted); + + AddUntilStep("retained selection", () => songSelect.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID); + } + [Test] public void TestPresentNewRulesetNewBeatmap() { From 3ff2058975a72aa69b2d2a7d39fa0ec4be2fe7d0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 29 Aug 2022 09:23:53 +0300 Subject: [PATCH 401/532] Fix back-to-front fallback comparison in `HitObjectOrderedSelectionContainer` --- .../Compose/Components/HitObjectOrderedSelectionContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index 06af232111..e3934025e2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (result != 0) return result; } - return CompareReverseChildID(y, x); + return CompareReverseChildID(x, y); } protected override void Dispose(bool isDisposing) From b2e80ca7f0314f7478b340309632a900f5119ace Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 29 Aug 2022 15:27:19 +0900 Subject: [PATCH 402/532] Don't include misses in failed score statistics --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 5b21caee84..547b132345 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -468,22 +468,6 @@ namespace osu.Game.Rulesets.Scoring score.Passed = false; Rank.Value = ScoreRank.F; - Debug.Assert(maximumResultCounts != null); - - if (maximumResultCounts.TryGetValue(HitResult.LargeTickHit, out int maximumLargeTick)) - scoreResultCounts[HitResult.LargeTickMiss] = maximumLargeTick - scoreResultCounts.GetValueOrDefault(HitResult.LargeTickHit); - - if (maximumResultCounts.TryGetValue(HitResult.SmallTickHit, out int maximumSmallTick)) - scoreResultCounts[HitResult.SmallTickMiss] = maximumSmallTick - scoreResultCounts.GetValueOrDefault(HitResult.SmallTickHit); - - int maximumBonusOrIgnore = maximumResultCounts.Where(kvp => kvp.Key.IsBonus() || kvp.Key == HitResult.IgnoreHit).Sum(kvp => kvp.Value); - int currentBonusOrIgnore = scoreResultCounts.Where(kvp => kvp.Key.IsBonus() || kvp.Key == HitResult.IgnoreHit).Sum(kvp => kvp.Value); - scoreResultCounts[HitResult.IgnoreMiss] = maximumBonusOrIgnore - currentBonusOrIgnore; - - int maximumBasic = maximumResultCounts.SingleOrDefault(kvp => kvp.Key.IsBasic()).Value; - int currentBasic = scoreResultCounts.Where(kvp => kvp.Key.IsBasic() && kvp.Key != HitResult.Miss).Sum(kvp => kvp.Value); - scoreResultCounts[HitResult.Miss] = maximumBasic - currentBasic; - PopulateScore(score); } From 423f6f90f2d28bf81ff5096b4b3a8156e7ad9caa Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Aug 2022 21:31:30 +0900 Subject: [PATCH 403/532] Remove async calls from ScoreManager --- .../TestScenePlayerLocalScoreImport.cs | 2 +- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 2 +- .../SongSelect/TestSceneTopLocalRank.cs | 2 +- .../TestSceneDeleteLocalScore.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- .../BeatmapSet/Scores/ScoresContainer.cs | 28 ++--- osu.Game/Scoring/ScoreManager.cs | 101 ++---------------- .../Playlists/PlaylistsResultsScreen.cs | 33 +++--- .../Expanded/ExpandedPanelMiddleContent.cs | 4 +- osu.Game/Screens/Ranking/ScorePanelList.cs | 45 ++++---- .../Select/Leaderboards/BeatmapLeaderboard.cs | 28 ++--- 11 files changed, 62 insertions(+), 187 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index ddb585a73c..f3e436e31f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay { Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(new ScoreManager(rulesets, () => beatmaps, LocalStorage, Realm, Scheduler, API)); + Dependencies.Cache(new ScoreManager(rulesets, () => beatmaps, LocalStorage, Realm, API)); Dependencies.Cache(Realm); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index aeb30c94e1..07da1790c8 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.SongSelect dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); - dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, Scheduler, API)); + dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); Dependencies.Cache(Realm); return dependencies; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs index 72d78ededb..086af3084d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.SongSelect { Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Realm, Scheduler, API)); + Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Realm, API)); Dependencies.Cache(Realm); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 3beade9d4f..db380cfdb7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.UserInterface dependencies.Cache(new RealmRulesetStore(Realm)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); - dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Realm, Scheduler, API)); + dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Realm, API)); Dependencies.Cache(Realm); return dependencies; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8d5c58d5f0..f87f95efd5 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -273,7 +273,7 @@ namespace osu.Game dependencies.Cache(difficultyCache = new BeatmapDifficultyCache()); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() - dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, API, difficultyCache, LocalConfig)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, API, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true)); diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index e50fc356eb..53818bbee3 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -6,10 +6,8 @@ using System.Diagnostics; using System.Linq; using System.Threading; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -87,27 +85,19 @@ namespace osu.Game.Overlays.BeatmapSet.Scores MD5Hash = apiBeatmap.MD5Hash }; - scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo)).ToArray(), loadCancellationSource.Token) - .ContinueWith(task => Schedule(() => - { - if (loadCancellationSource.IsCancellationRequested) - return; + var scores = scoreManager.OrderByTotalScore(value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo))).ToArray(); + var topScore = scores.First(); - var scores = task.GetResultSafely(); + scoreTable.DisplayScores(scores, apiBeatmap.Status.GrantsPerformancePoints()); + scoreTable.Show(); - var topScore = scores.First(); + var userScore = value.UserScore; + var userScoreInfo = userScore?.Score.ToScoreInfo(rulesets, beatmapInfo); - scoreTable.DisplayScores(scores, apiBeatmap.Status.GrantsPerformancePoints()); - scoreTable.Show(); + topScoresContainer.Add(new DrawableTopScore(topScore)); - var userScore = value.UserScore; - var userScoreInfo = userScore?.Score.ToScoreInfo(rulesets, beatmapInfo); - - topScoresContainer.Add(new DrawableTopScore(topScore)); - - if (userScoreInfo != null && userScoreInfo.OnlineID != topScore.OnlineID) - topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position)); - }), TaskContinuationOptions.OnlyOnRanToCompletion); + if (userScoreInfo != null && userScoreInfo.OnlineID != topScore.OnlineID) + topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position)); }); } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index ecd37c761c..7204b5a281 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -11,10 +11,7 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Bindables; -using osu.Framework.Extensions; -using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; @@ -28,17 +25,12 @@ namespace osu.Game.Scoring { public class ScoreManager : ModelManager, IModelImporter { - private readonly Scheduler scheduler; - private readonly BeatmapDifficultyCache difficultyCache; private readonly OsuConfigManager configManager; private readonly ScoreImporter scoreImporter; - public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler, IAPIProvider api, - BeatmapDifficultyCache difficultyCache = null, OsuConfigManager configManager = null) + public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, IAPIProvider api, OsuConfigManager configManager = null) : base(storage, realm) { - this.scheduler = scheduler; - this.difficultyCache = difficultyCache; this.configManager = configManager; scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm, api) @@ -63,27 +55,13 @@ namespace osu.Game.Scoring /// Orders an array of s by total score. /// /// The array of s to reorder. - /// A to cancel the process. /// The given ordered by decreasing total score. - public async Task OrderByTotalScoreAsync(ScoreInfo[] scores, CancellationToken cancellationToken = default) + public IEnumerable OrderByTotalScore(IEnumerable scores) { - if (difficultyCache != null) - { - // Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below. - foreach (var s in scores) - { - await difficultyCache.GetDifficultyAsync(s.BeatmapInfo, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - } - } - - long[] totalScores = await Task.WhenAll(scores.Select(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken))).ConfigureAwait(false); - - return scores.Select((score, index) => (score, totalScore: totalScores[index])) + return scores.Select((score, index) => (score, totalScore: GetTotalScore(score))) .OrderByDescending(g => g.totalScore) .ThenBy(g => g.score.OnlineID) - .Select(g => g.score) - .ToArray(); + .Select(g => g.score); } /// @@ -106,44 +84,18 @@ namespace osu.Game.Scoring /// The bindable containing the formatted total score string. public Bindable GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score)); - /// - /// Retrieves the total score of a in the given . - /// The score is returned in a callback that is run on the update thread. - /// - /// The to calculate the total score of. - /// The callback to be invoked with the total score. - /// The to return the total score as. - /// A to cancel the process. - public void GetTotalScore([NotNull] ScoreInfo score, [NotNull] Action callback, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default) - { - GetTotalScoreAsync(score, mode, cancellationToken) - .ContinueWith(task => scheduler.Add(() => - { - if (!cancellationToken.IsCancellationRequested) - callback(task.GetResultSafely()); - }), TaskContinuationOptions.OnlyOnRanToCompletion); - } - /// /// Retrieves the total score of a in the given . /// /// The to calculate the total score of. /// The to return the total score as. - /// A to cancel the process. /// The total score. - public async Task GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default) + public long GetTotalScore([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised) { // TODO: This is required for playlist aggregate scores. They should likely not be getting here in the first place. if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash)) return score.TotalScore; - int? beatmapMaxCombo = await GetMaximumAchievableComboAsync(score, cancellationToken).ConfigureAwait(false); - if (beatmapMaxCombo == null) - return score.TotalScore; - - if (beatmapMaxCombo == 0) - return 0; - var ruleset = score.Ruleset.CreateInstance(); var scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = score.Mods; @@ -155,33 +107,9 @@ namespace osu.Game.Scoring /// Retrieves the maximum achievable combo for the provided score. /// /// The to compute the maximum achievable combo for. - /// A to cancel the process. /// The maximum achievable combo. A return value indicates the difficulty cache has failed to retrieve the combo. - public async Task GetMaximumAchievableComboAsync([NotNull] ScoreInfo score, CancellationToken cancellationToken = default) + public int GetMaximumAchievableCombo([NotNull] ScoreInfo score) { - if (score.IsLegacyScore) - { - // This score is guaranteed to be an osu!stable score. - // The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used. -#pragma warning disable CS0618 - if (score.BeatmapInfo.MaxCombo != null) - return score.BeatmapInfo.MaxCombo.Value; -#pragma warning restore CS0618 - - if (difficultyCache == null) - return null; - - // We can compute the max combo locally after the async beatmap difficulty computation. - var difficulty = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false); - - if (difficulty == null) - Logger.Log($"Couldn't get beatmap difficulty for beatmap {score.BeatmapInfo.OnlineID}"); - - return difficulty?.MaxCombo; - } - - // This is guaranteed to be a non-legacy score. - // The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values. return Enum.GetValues(typeof(HitResult)).OfType().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum(); } @@ -191,10 +119,6 @@ namespace osu.Game.Scoring private class TotalScoreBindable : Bindable { private readonly Bindable scoringMode = new Bindable(); - private readonly ScoreInfo score; - private readonly ScoreManager scoreManager; - - private CancellationTokenSource difficultyCalculationCancellationSource; /// /// Creates a new . @@ -204,19 +128,8 @@ namespace osu.Game.Scoring /// The config. public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager, OsuConfigManager configManager) { - this.score = score; - this.scoreManager = scoreManager; - configManager?.BindWith(OsuSetting.ScoreDisplayMode, scoringMode); - scoringMode.BindValueChanged(onScoringModeChanged, true); - } - - private void onScoringModeChanged(ValueChangedEvent mode) - { - difficultyCalculationCancellationSource?.Cancel(); - difficultyCalculationCancellationSource = new CancellationTokenSource(); - - scoreManager.GetTotalScore(score, s => Value = s, mode.NewValue, difficultyCalculationCancellationSource.Token); + scoringMode.BindValueChanged(mode => Value = scoreManager.GetTotalScore(score, mode.NewValue), true); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs index 53b38962ac..41633c34ce 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs @@ -180,31 +180,26 @@ namespace osu.Game.Screens.OnlinePlay.Playlists /// The callback to invoke with the final s. /// The s that were retrieved from s. /// An optional pivot around which the scores were retrieved. - private void performSuccessCallback([NotNull] Action> callback, [NotNull] List scores, [CanBeNull] MultiplayerScores pivot = null) + private void performSuccessCallback([NotNull] Action> callback, [NotNull] List scores, [CanBeNull] MultiplayerScores pivot = null) => Schedule(() => { - var scoreInfos = scores.Select(s => s.CreateScoreInfo(rulesets, playlistItem, Beatmap.Value.BeatmapInfo)).ToArray(); + var scoreInfos = scoreManager.OrderByTotalScore(scores.Select(s => s.CreateScoreInfo(rulesets, playlistItem, Beatmap.Value.BeatmapInfo))).ToArray(); - // Score panels calculate total score before displaying, which can take some time. In order to count that calculation as part of the loading spinner display duration, - // calculate the total scores locally before invoking the success callback. - scoreManager.OrderByTotalScoreAsync(scoreInfos).ContinueWith(_ => Schedule(() => + // Select a score if we don't already have one selected. + // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). + if (SelectedScore.Value == null) { - // Select a score if we don't already have one selected. - // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). - if (SelectedScore.Value == null) + Schedule(() => { - Schedule(() => - { - // Prefer selecting the local user's score, or otherwise default to the first visible score. - SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault(); - }); - } + // Prefer selecting the local user's score, or otherwise default to the first visible score. + SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault(); + }); + } - // Invoke callback to add the scores. Exclude the user's current score which was added previously. - callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); + // Invoke callback to add the scores. Exclude the user's current score which was added previously. + callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID)); - hideLoadingSpinners(pivot); - })); - } + hideLoadingSpinners(pivot); + }); private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null) { diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 0f202e5e08..b496f4242d 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -67,12 +67,10 @@ namespace osu.Game.Screens.Ranking.Expanded var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; string creator = metadata.Author.Username; - int? beatmapMaxCombo = scoreManager.GetMaximumAchievableComboAsync(score).GetResultSafely(); - var topStatistics = new List { new AccuracyStatistic(score.Accuracy), - new ComboStatistic(score.MaxCombo, beatmapMaxCombo), + new ComboStatistic(score.MaxCombo, scoreManager.GetMaximumAchievableCombo(score)), new PerformanceStatistic(score), }; diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 4f9e61a4a1..46f9efd126 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -8,11 +8,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; @@ -151,32 +149,27 @@ namespace osu.Game.Screens.Ranking var score = trackingContainer.Panel.Score; - // Calculating score can take a while in extreme scenarios, so only display scores after the process completes. - scoreManager.GetTotalScoreAsync(score) - .ContinueWith(task => Schedule(() => - { - flow.SetLayoutPosition(trackingContainer, task.GetResultSafely()); + flow.SetLayoutPosition(trackingContainer, scoreManager.GetTotalScore(score)); - trackingContainer.Show(); + trackingContainer.Show(); - if (SelectedScore.Value?.Equals(score) == true) - { - SelectedScore.TriggerChange(); - } - else - { - // We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done. - // But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel. - if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score)) - { - // A somewhat hacky property is used here because we need to: - // 1) Scroll after the scroll container's visible range is updated. - // 2) Scroll before the scroll container's scroll position is updated. - // Without this, we would have a 1-frame positioning error which looks very jarring. - scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing; - } - } - }), TaskContinuationOptions.OnlyOnRanToCompletion); + if (SelectedScore.Value?.Equals(score) == true) + { + SelectedScore.TriggerChange(); + } + else + { + // We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done. + // But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel. + if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score)) + { + // A somewhat hacky property is used here because we need to: + // 1) Scroll after the scroll container's visible range is updated. + // 2) Scroll before the scroll container's scroll position is updated. + // Without this, we would have a 1-frame positioning error which looks very jarring. + scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing; + } + } } /// diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index b497943dfa..343b815e9f 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -8,10 +8,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Extensions; @@ -150,17 +148,12 @@ namespace osu.Game.Screens.Select.Leaderboards var req = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods); - req.Success += r => + req.Success += r => Schedule(() => { - scoreManager.OrderByTotalScoreAsync(r.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo)).ToArray(), cancellationToken) - .ContinueWith(task => Schedule(() => - { - if (cancellationToken.IsCancellationRequested) - return; - - SetScores(task.GetResultSafely(), r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo)); - }), TaskContinuationOptions.OnlyOnRanToCompletion); - }; + SetScores( + scoreManager.OrderByTotalScore(r.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo))), + r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo)); + }); return req; } @@ -213,16 +206,9 @@ namespace osu.Game.Screens.Select.Leaderboards scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym))); } - scores = scores.Detach(); + scores = scoreManager.OrderByTotalScore(scores.Detach()); - scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken) - .ContinueWith(ordered => Schedule(() => - { - if (cancellationToken.IsCancellationRequested) - return; - - SetScores(ordered.GetResultSafely()); - }), TaskContinuationOptions.OnlyOnRanToCompletion); + Schedule(() => SetScores(scores)); } } From d75543ad68acc74546d9e4ab4d1c2066efb52d44 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 29 Aug 2022 15:36:10 +0900 Subject: [PATCH 404/532] Simplify GetMaximumAchievableCombo further --- osu.Game/Scoring/ScoreManager.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 7204b5a281..fd600f4864 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -107,11 +107,8 @@ namespace osu.Game.Scoring /// Retrieves the maximum achievable combo for the provided score. /// /// The to compute the maximum achievable combo for. - /// The maximum achievable combo. A return value indicates the difficulty cache has failed to retrieve the combo. - public int GetMaximumAchievableCombo([NotNull] ScoreInfo score) - { - return Enum.GetValues(typeof(HitResult)).OfType().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum(); - } + /// The maximum achievable combo. + public int GetMaximumAchievableCombo([NotNull] ScoreInfo score) => score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value); /// /// Provides the total score of a . Responds to changes in the currently-selected . From 81ac0daba8cd9574fd08821cbf8fe767d0969a1b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 29 Aug 2022 15:51:12 +0900 Subject: [PATCH 405/532] Update xmldoc --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 547b132345..cd01ae7eff 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -458,7 +458,7 @@ namespace osu.Game.Rulesets.Scoring } /// - /// Populates the given score with remaining statistics as "missed" and marks it with rank. + /// Populates a failed score, marking it with the rank. /// public void FailScore(ScoreInfo score) { From a215d009fed8bae6f3566e08f19f3de753c098e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 15:19:05 +0900 Subject: [PATCH 406/532] Update `Remove`/`RemoveRange`/`RemoveAll` calls in line with framework changes --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 6 +++--- osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs | 6 +++--- osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs | 2 +- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs | 4 ++-- .../Objects/Drawables/DrawableOsuHitObject.cs | 2 +- .../Objects/Drawables/DrawableTaikoHitObject.cs | 6 +++--- .../Visual/Background/TestSceneSeasonalBackgroundLoader.cs | 2 +- .../Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs | 4 ++-- osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs | 4 ++-- osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs | 2 +- .../Visual/Online/TestSceneLeaderboardModSelector.cs | 2 +- .../Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs | 2 +- .../Visual/UserInterface/TestSceneLogoTrackingContainer.cs | 2 +- .../Screens/TestSceneGameplayScreen.cs | 2 +- osu.Game.Tournament/Screens/Drawings/Components/Group.cs | 2 +- .../Screens/Drawings/Components/ScrollingTeamContainer.cs | 4 ++-- .../Screens/Drawings/Components/VisualiserContainer.cs | 2 +- .../Screens/Editors/TournamentEditorScreen.cs | 2 +- osu.Game/Graphics/Containers/SectionsContainer.cs | 2 +- .../Graphics/Containers/SelectionCycleFillFlowContainer.cs | 2 +- osu.Game/Graphics/UserInterface/BarGraph.cs | 2 +- osu.Game/Graphics/UserInterface/TwoLayerButton.cs | 4 ++-- osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 2 +- osu.Game/Overlays/Chat/DrawableChannel.cs | 2 +- osu.Game/Rulesets/UI/JudgementContainer.cs | 2 +- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 2 +- .../Components/HitObjectOrderedSelectionContainer.cs | 4 ++-- .../Components/Timeline/TimelineBlueprintContainer.cs | 2 +- osu.Game/Screens/Menu/IntroTriangles.cs | 3 +-- .../Lounge/Components/DrawableRoomParticipantsList.cs | 4 ++-- .../Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs | 2 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- osu.Game/Skinning/SkinnableSound.cs | 2 +- osu.Game/Skinning/SkinnableTargetContainer.cs | 2 +- osu.Game/Tests/Visual/OsuGameTestScene.cs | 5 +---- osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs | 2 +- 39 files changed, 52 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index efc841dfac..61fefcfd99 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -271,8 +271,8 @@ namespace osu.Game.Rulesets.Catch.UI SetHyperDashState(); } - caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); - droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject); + caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject, false); + droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject, false); } /// @@ -430,7 +430,7 @@ namespace osu.Game.Rulesets.Catch.UI { var droppedObject = getDroppedObject(caughtObject); - caughtObjectContainer.Remove(caughtObject); + caughtObjectContainer.Remove(caughtObject, false); droppedObjectTarget.Add(droppedObject); diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs index 6ab6a59293..d255937fed 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs @@ -93,15 +93,15 @@ namespace osu.Game.Rulesets.Catch.UI switch (entry.Animation) { case CatcherTrailAnimation.Dashing: - dashTrails.Remove(drawable); + dashTrails.Remove(drawable, false); break; case CatcherTrailAnimation.HyperDashing: - hyperDashTrails.Remove(drawable); + hyperDashTrails.Remove(drawable, false); break; case CatcherTrailAnimation.HyperDashAfterImage: - hyperDashAfterImages.Remove(drawable); + hyperDashAfterImages.Remove(drawable, false); break; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs index 3c24e91d54..6a94e5d371 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Mods HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; Container hocParent = (Container)hoc.Parent; - hocParent.Remove(hoc); + hocParent.Remove(hoc, false); hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c => { c.RelativeSizeAxes = Axes.Both; diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index 4b6f364831..49ba503cb5 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy else { lightContainer.FadeOut(120) - .OnComplete(d => Column.TopLevelContainer.Remove(d)); + .OnComplete(d => Column.TopLevelContainer.Remove(d, false)); } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs index 305678bb62..1e625cd4e6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs @@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Tests { var drawableObject = getFunc.Invoke(); - hitObjectContainer.Remove(drawableObject); + hitObjectContainer.Remove(drawableObject, false); followPointRenderer.RemoveFollowPoints(drawableObject.HitObject); }); } @@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Tests else targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1; - hitObjectContainer.Remove(toReorder); + hitObjectContainer.Remove(toReorder, false); toReorder.HitObject.StartTime = targetTime; hitObjectContainer.Add(toReorder); }); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 360b28f69f..aedb62dd24 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // This is a bit ugly but we don't have the concept of InternalContent so it'll have to do for now. (https://github.com/ppy/osu-framework/issues/1690) protected override void AddInternal(Drawable drawable) => shakeContainer.Add(drawable); protected override void ClearInternal(bool disposeChildren = true) => shakeContainer.Clear(disposeChildren); - protected override bool RemoveInternal(Drawable drawable) => shakeContainer.Remove(drawable); + protected override bool RemoveInternal(Drawable drawable) => shakeContainer.Remove(drawable, true); protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 9bbd3670fa..c0c80eaa4a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables isProxied = true; - nonProxiedContent.Remove(Content); + nonProxiedContent.Remove(Content, false); proxiedContent.Add(Content); } @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables isProxied = false; - proxiedContent.Remove(Content); + proxiedContent.Remove(Content, false); nonProxiedContent.Add(Content); } @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Size = BaseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE); if (MainPiece != null) - Content.Remove(MainPiece); + Content.Remove(MainPiece, true); Content.Add(MainPiece = CreateMainPiece()); } diff --git a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs index cce7ae1922..c9920cd01f 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs @@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.Background => AddStep("create loader", () => { if (backgroundLoader != null) - Remove(backgroundLoader); + Remove(backgroundLoader, true); Add(backgroundLoader = new SeasonalBackgroundLoader()); }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index d6c49b026e..5c7321fb24 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Gameplay Add(expectedComponentsAdjustmentContainer); expectedComponentsAdjustmentContainer.UpdateSubTree(); var expectedInfo = expectedComponentsContainer.CreateSkinnableInfo(); - Remove(expectedComponentsAdjustmentContainer); + Remove(expectedComponentsAdjustmentContainer, true); return almostEqual(actualInfo, expectedInfo); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs index 9ad8ac086c..083be3539d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay { base.TearDownSteps(); AddStep("stop watching user", () => spectatorClient.StopWatchingUser(dummy_user_id)); - AddStep("remove test spectator client", () => Remove(spectatorClient)); + AddStep("remove test spectator client", () => Remove(spectatorClient, false)); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 30c2790fb4..2ec675874b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void loadStoryboard(IWorkingBeatmap working) { if (storyboard != null) - storyboardContainer.Remove(storyboard); + storyboardContainer.Remove(storyboard, true); var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; storyboardContainer.Clock = decoupledClock; @@ -106,7 +106,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void loadStoryboardNoVideo() { if (storyboard != null) - storyboardContainer.Remove(storyboard); + storyboardContainer.Remove(storyboard, true); var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; storyboardContainer.Clock = decoupledClock; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index 90e218675c..4473f315b9 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -62,8 +62,8 @@ namespace osu.Game.Tests.Visual.Menus [SetUp] public void SetUp() => Schedule(() => { - Remove(nowPlayingOverlay); - Remove(volumeOverlay); + Remove(nowPlayingOverlay, false); + Remove(volumeOverlay, false); Children = new Drawable[] { diff --git a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs index f5c7ee2f19..2879536034 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("force save config", () => Game.LocalConfig.Save()); - AddStep("remove game", () => Remove(Game)); + AddStep("remove game", () => Remove(Game, true)); AddStep("create game again", CreateGame); diff --git a/osu.Game.Tests/Visual/Online/TestSceneLeaderboardModSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneLeaderboardModSelector.cs index 10d9a5664e..5579ecedbd 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneLeaderboardModSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneLeaderboardModSelector.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Online { if (selected.Text == mod.Acronym) { - selectedMods.Remove(selected); + selectedMods.Remove(selected, true); break; } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs index 27b485156c..c42b51c1a6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs @@ -98,7 +98,7 @@ namespace osu.Game.Tests.Visual.SongSelect OsuLogo logo = new OsuLogo { Scale = new Vector2(0.15f) }; - Remove(testDifficultyCache); + Remove(testDifficultyCache, false); Children = new Drawable[] { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs index 558ea01a49..d069e742dd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs @@ -259,7 +259,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void removeFacade() { - trackingContainer.Remove(logoFacade); + trackingContainer.Remove(logoFacade, false); visualBox.Colour = Color4.White; moveLogoFacade(); } diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index 842324d03d..4fc15c365f 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tournament.Tests.Screens { AddStep("setup screen", () => { - Remove(chat); + Remove(chat, false); Children = new Drawable[] { diff --git a/osu.Game.Tournament/Screens/Drawings/Components/Group.cs b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs index f50abd6e58..0b1a5328ab 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/Group.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs @@ -93,7 +93,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components { allTeams.RemoveAll(gt => gt.Team == team); - if (teams.RemoveAll(gt => gt.Team == team) > 0) + if (teams.RemoveAll(gt => gt.Team == team, true) > 0) { TeamsCount--; return true; diff --git a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs index 52c611d323..55555adb80 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs @@ -170,7 +170,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components availableTeams.Add(team); - RemoveAll(c => c is ScrollingTeam); + RemoveAll(c => c is ScrollingTeam, false); setScrollState(ScrollState.Idle); } @@ -186,7 +186,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components public void ClearTeams() { availableTeams.Clear(); - RemoveAll(c => c is ScrollingTeam); + RemoveAll(c => c is ScrollingTeam, true); setScrollState(ScrollState.Idle); } diff --git a/osu.Game.Tournament/Screens/Drawings/Components/VisualiserContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/VisualiserContainer.cs index 15edbb76c1..663162d1ca 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/VisualiserContainer.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/VisualiserContainer.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components if (allLines.Count == 0) return; - Remove(allLines.First()); + Remove(allLines.First(), true); allLines.Remove(allLines.First()); } diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs index 0fefe6f780..8c55026c67 100644 --- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs @@ -106,7 +106,7 @@ namespace osu.Game.Tournament.Screens.Editors break; case NotifyCollectionChangedAction.Remove: - args.OldItems.Cast().ForEach(i => flow.RemoveAll(d => d.Model == i)); + args.OldItems.Cast().ForEach(i => flow.RemoveAll(d => d.Model == i, true)); break; } }; diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index c04a5add89..448ecc6b3d 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -70,7 +70,7 @@ namespace osu.Game.Graphics.Containers if (value == footer) return; if (footer != null) - scrollContainer.Remove(footer); + scrollContainer.Remove(footer, true); footer = value; if (value == null) return; diff --git a/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs b/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs index 94505d2310..2667b8b8e0 100644 --- a/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs +++ b/osu.Game/Graphics/Containers/SelectionCycleFillFlowContainer.cs @@ -59,7 +59,7 @@ namespace osu.Game.Graphics.Containers drawable.StateChanged += state => selectionChanged(drawable, state); } - public override bool Remove(T drawable) + public override bool Remove(T drawable, bool disposeImmediately) => throw new NotSupportedException($"Cannot remove drawables from {nameof(SelectionCycleFillFlowContainer)}"); private void setSelected(int? value) diff --git a/osu.Game/Graphics/UserInterface/BarGraph.cs b/osu.Game/Graphics/UserInterface/BarGraph.cs index f55875ac58..2e9fd6734f 100644 --- a/osu.Game/Graphics/UserInterface/BarGraph.cs +++ b/osu.Game/Graphics/UserInterface/BarGraph.cs @@ -74,7 +74,7 @@ namespace osu.Game.Graphics.UserInterface } //I'm using ToList() here because Where() returns an Enumerable which can change it's elements afterwards - RemoveRange(Children.Where((_, index) => index >= value.Count()).ToList()); + RemoveRange(Children.Where((_, index) => index >= value.Count()).ToList(), true); } } } diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs index 144b51d3e8..1b8848f3d5 100644 --- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs +++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs @@ -64,8 +64,8 @@ namespace osu.Game.Graphics.UserInterface X = value.HasFlagFast(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0; - Remove(c1); - Remove(c2); + Remove(c1, false); + Remove(c2, false); c1.Depth = value.HasFlagFast(Anchor.x2) ? 0 : 1; c2.Depth = value.HasFlagFast(Anchor.x2) ? 1 : 0; Add(c1); diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index b3814ca90c..d9f962ca97 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -99,7 +99,7 @@ namespace osu.Game.Overlays.Chat.ChannelList FillFlowContainer flow = getFlowForChannel(channel); channelMap.Remove(channel); - flow.Remove(item); + flow.Remove(item, true); updateVisibility(); } diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index c05f456a96..544daf7d2c 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -181,7 +181,7 @@ namespace osu.Game.Overlays.Chat { Trace.Assert(updated.Id.HasValue, "An updated message was returned with no ID."); - ChatLineFlow.Remove(found); + ChatLineFlow.Remove(found, false); found.Message = updated; ChatLineFlow.Add(found); } diff --git a/osu.Game/Rulesets/UI/JudgementContainer.cs b/osu.Game/Rulesets/UI/JudgementContainer.cs index 4336977aa8..471a62cab3 100644 --- a/osu.Game/Rulesets/UI/JudgementContainer.cs +++ b/osu.Game/Rulesets/UI/JudgementContainer.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.UI // remove any existing judgements for the judged object. // this can be the case when rewinding. - RemoveAll(c => c.JudgedObject == judgement.JudgedObject); + RemoveAll(c => c.JudgedObject == judgement.JudgedObject, false); base.Add(judgement); } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 540fbf9a72..8b38d9c612 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -292,7 +292,7 @@ namespace osu.Game.Screens.Edit.Compose.Components blueprint.Selected -= OnBlueprintSelected; blueprint.Deselected -= OnBlueprintDeselected; - SelectionBlueprints.Remove(blueprint); + SelectionBlueprints.Remove(blueprint, true); if (movementBlueprints?.Contains(blueprint) == true) finishSelectionMovement(); diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index 06af232111..3be1c27129 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -35,10 +35,10 @@ namespace osu.Game.Screens.Edit.Compose.Components base.Add(drawable); } - public override bool Remove(SelectionBlueprint drawable) + public override bool Remove(SelectionBlueprint drawable, bool disposeImmediately) { SortInternal(); - return base.Remove(drawable); + return base.Remove(drawable, disposeImmediately); } protected override int Compare(Drawable x, Drawable y) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 742e16d5a9..590f92d281 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { if (placementBlueprint != null) { - SelectionBlueprints.Remove(placementBlueprint); + SelectionBlueprints.Remove(placementBlueprint, true); placementBlueprint = null; } } diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index d777f78df2..05a6d25303 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -255,8 +255,7 @@ namespace osu.Game.Screens.Menu { lazerLogo.FadeOut().OnComplete(_ => { - logoContainerSecondary.Remove(lazerLogo); - lazerLogo.Dispose(); // explicit disposal as we are pushing a new screen and the expire may not get run. + logoContainerSecondary.Remove(lazerLogo, true); logo.FadeIn(); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index 995a0d3397..9e2bd41fd0 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -232,7 +232,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private void removeUser(APIUser user) { - avatarFlow.RemoveAll(a => a.User == user); + avatarFlow.RemoveAll(a => a.User == user, true); } private void clearUsers() @@ -250,7 +250,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components hiddenUsers.Count = hiddenCount; if (displayedCircles > NumberOfCircles) - avatarFlow.Remove(avatarFlow.Last()); + avatarFlow.Remove(avatarFlow.Last(), true); else if (displayedCircles < NumberOfCircles) { var nextUser = RecentParticipants.FirstOrDefault(u => avatarFlow.All(a => a.User != u)); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 6142fc78a8..e6b1942506 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -138,7 +138,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { foreach (var r in rooms) { - roomFlow.RemoveAll(d => d.Room == r); + roomFlow.RemoveAll(d => d.Room == r, true); // selection may have a lease due to being in a sub screen. if (!SelectedRoom.Disabled) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 226216b0f0..486df8653f 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -317,7 +317,7 @@ namespace osu.Game.Screens.Ranking var screenSpacePos = detachedPanel.ScreenSpaceDrawQuad.TopLeft; // Remove from the local container and re-attach. - detachedPanelContainer.Remove(detachedPanel); + detachedPanelContainer.Remove(detachedPanel, false); ScorePanelList.Attach(detachedPanel); // Move into its original location in the attached container first, then to the final location. diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index c2696c56f3..8f71b40801 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -151,7 +151,7 @@ namespace osu.Game.Skinning bool wasPlaying = IsPlaying; // Remove all pooled samples (return them to the pool), and dispose the rest. - samplesContainer.RemoveAll(s => s.IsInPool); + samplesContainer.RemoveAll(s => s.IsInPool, false); samplesContainer.Clear(); foreach (var s in samples) diff --git a/osu.Game/Skinning/SkinnableTargetContainer.cs b/osu.Game/Skinning/SkinnableTargetContainer.cs index 341a881789..2faaa9a905 100644 --- a/osu.Game/Skinning/SkinnableTargetContainer.cs +++ b/osu.Game/Skinning/SkinnableTargetContainer.cs @@ -85,7 +85,7 @@ namespace osu.Game.Skinning if (!(component is Drawable drawable)) throw new ArgumentException($"Provided argument must be of type {nameof(Drawable)}.", nameof(component)); - content.Remove(drawable); + content.Remove(drawable, true); components.Remove(component); } diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index 69a945db34..e47d19fba6 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -58,10 +58,7 @@ namespace osu.Game.Tests.Visual AddStep("Create new game instance", () => { if (Game?.Parent != null) - { - Remove(Game); - Game.Dispose(); - } + Remove(Game, true); RecycleLocalStorage(false); diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index 797e8363c3..a12459bc4e 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual protected void ResetPlacement() { if (CurrentBlueprint != null) - Remove(CurrentBlueprint); + Remove(CurrentBlueprint, true); Add(CurrentBlueprint = CreateBlueprint()); } From 105aa01e7d414d5e1837d00fd7eb4990895d98c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 15:49:28 +0900 Subject: [PATCH 407/532] Update usages of `RemoveInternal` --- .../Edit/Blueprints/Components/EditablePath.cs | 2 +- .../Edit/Blueprints/Components/NestedOutlineContainer.cs | 2 +- .../Objects/Drawables/DrawableOsuHitObject.cs | 2 +- osu.Game/Graphics/Backgrounds/Background.cs | 2 +- osu.Game/Graphics/Containers/SectionsContainer.cs | 2 +- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 2 +- .../Objects/Pooling/PooledDrawableWithLifetimeContainer.cs | 2 +- osu.Game/Rulesets/UI/HitObjectContainer.cs | 2 +- osu.Game/Screens/Play/FailAnimation.cs | 3 +-- osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs | 2 +- 10 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index 44d14ec330..8473eda663 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject) { while (path.Vertices.Count < InternalChildren.Count) - RemoveInternal(InternalChildren[^1]); + RemoveInternal(InternalChildren[^1], true); while (InternalChildren.Count < path.Vertices.Count) AddInternal(new VertexPiece()); diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs index 431ba331ac..a6f1732bc1 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components .Where(h => !(h is TinyDroplet))); while (nestedHitObjects.Count < InternalChildren.Count) - RemoveInternal(InternalChildren[^1]); + RemoveInternal(InternalChildren[^1], true); while (InternalChildren.Count < nestedHitObjects.Count) AddInternal(new FruitOutline()); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index aedb62dd24..6e525071ca 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // This is a bit ugly but we don't have the concept of InternalContent so it'll have to do for now. (https://github.com/ppy/osu-framework/issues/1690) protected override void AddInternal(Drawable drawable) => shakeContainer.Add(drawable); protected override void ClearInternal(bool disposeChildren = true) => shakeContainer.Clear(disposeChildren); - protected override bool RemoveInternal(Drawable drawable) => shakeContainer.Remove(drawable, true); + protected override bool RemoveInternal(Drawable drawable, bool disposeImmediately) => shakeContainer.Remove(drawable, disposeImmediately); protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt; diff --git a/osu.Game/Graphics/Backgrounds/Background.cs b/osu.Game/Graphics/Backgrounds/Background.cs index 2dd96bcb09..0899c0706d 100644 --- a/osu.Game/Graphics/Backgrounds/Background.cs +++ b/osu.Game/Graphics/Backgrounds/Background.cs @@ -57,7 +57,7 @@ namespace osu.Game.Graphics.Backgrounds { if (bufferedContainer == null && newBlurSigma != Vector2.Zero) { - RemoveInternal(Sprite); + RemoveInternal(Sprite, false); AddInternal(bufferedContainer = new BufferedContainer(cachedFrameBuffer: true) { diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 448ecc6b3d..b8a8ea79cc 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Graphics.Containers if (value == expandableHeader) return; if (expandableHeader != null) - RemoveInternal(expandableHeader); + RemoveInternal(expandableHeader, false); expandableHeader = value; diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 0d8b8429d8..7e1196d4ca 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Judgements // sub-classes might have added their own children that would be removed here if .InternalChild was used. if (JudgementBody != null) - RemoveInternal(JudgementBody); + RemoveInternal(JudgementBody, true); AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent(type), _ => CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling) diff --git a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs index 07a80895e6..d5b4390ce8 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Objects.Pooling /// /// Invoked when the entry became dead. /// - protected virtual void RemoveDrawable(TEntry entry, TDrawable drawable) => RemoveInternal(drawable); + protected virtual void RemoveDrawable(TEntry entry, TDrawable drawable) => RemoveInternal(drawable, false); private void entryCrossedBoundary(LifetimeEntry lifetimeEntry, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction) { diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 589b585643..bbced9e58c 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.UI unbindStartTime(drawable); - RemoveInternal(drawable); + RemoveInternal(drawable, false); } #endregion diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 7275b369c3..a4b4bf4d2b 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -166,8 +166,7 @@ namespace osu.Game.Screens.Play if (filters.Parent == null) return; - RemoveInternal(filters); - filters.Dispose(); + RemoveInternal(filters, true); } protected override void Update() diff --git a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs index e55c4530b4..b4d6d481ef 100644 --- a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs +++ b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Ranking if (InternalChildren.Count == 0) throw new InvalidOperationException("Score panel container is not attached."); - RemoveInternal(Panel); + RemoveInternal(Panel, false); } /// From d4a37725c43d6582b89c2eba96d5732ee265785a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 29 Aug 2022 15:59:57 +0900 Subject: [PATCH 408/532] Adjust test --- .../Gameplay/TestSceneScoreProcessor.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index 4123412ab6..fb9d841d99 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -124,14 +124,19 @@ namespace osu.Game.Tests.Gameplay Assert.That(score.Rank, Is.EqualTo(ScoreRank.F)); Assert.That(score.Passed, Is.False); - Assert.That(score.Statistics.Count(kvp => kvp.Value > 0), Is.EqualTo(7)); + Assert.That(score.Statistics.Sum(kvp => kvp.Value), Is.EqualTo(4)); + Assert.That(score.MaximumStatistics.Sum(kvp => kvp.Value), Is.EqualTo(8)); + Assert.That(score.Statistics[HitResult.Ok], Is.EqualTo(1)); - Assert.That(score.Statistics[HitResult.Miss], Is.EqualTo(1)); Assert.That(score.Statistics[HitResult.LargeTickHit], Is.EqualTo(1)); - Assert.That(score.Statistics[HitResult.LargeTickMiss], Is.EqualTo(1)); - Assert.That(score.Statistics[HitResult.SmallTickMiss], Is.EqualTo(2)); + Assert.That(score.Statistics[HitResult.SmallTickMiss], Is.EqualTo(1)); Assert.That(score.Statistics[HitResult.SmallBonus], Is.EqualTo(1)); - Assert.That(score.Statistics[HitResult.IgnoreMiss], Is.EqualTo(1)); + + Assert.That(score.MaximumStatistics[HitResult.Perfect], Is.EqualTo(2)); + Assert.That(score.MaximumStatistics[HitResult.LargeTickHit], Is.EqualTo(2)); + Assert.That(score.MaximumStatistics[HitResult.SmallTickHit], Is.EqualTo(2)); + Assert.That(score.MaximumStatistics[HitResult.SmallBonus], Is.EqualTo(1)); + Assert.That(score.MaximumStatistics[HitResult.LargeBonus], Is.EqualTo(1)); } private class TestJudgement : Judgement From be5c6232e82ea1fa9115fda102a6259ec5bb2444 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 16:29:19 +0900 Subject: [PATCH 409/532] Encapsulate `Track` inside a `FramedClock` to avoid mutating operations --- osu.Game/OsuGameBase.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 27ea3a76ae..0b158d5e08 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -381,7 +381,16 @@ namespace osu.Game Beatmap.BindValueChanged(onBeatmapChanged); } - private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) => beatmapClock.ChangeSource(beatmap.Track); + private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) + { + // FramedBeatmapClock uses a decoupled clock internally which will mutate the source if it is an `IAdjustableClock`. + // We don't want this for now, as the intention of beatmapClock is to be a read-only source for beat sync components. + // + // Encapsulating in a FramedClock will avoid any mutations. + var framedClock = new FramedClock(beatmap.Track); + + beatmapClock.ChangeSource(framedClock); + } protected virtual void InitialiseFonts() { From cf8fad045d94a010d96c7764fc66879ae2f22561 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 16:32:41 +0900 Subject: [PATCH 410/532] Update template rulesets to include baked value --- .../osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs | 3 +++ .../osu.Game.Rulesets.Pippidon/PippidonRuleset.cs | 3 +++ .../osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs | 3 +++ .../osu.Game.Rulesets.Pippidon/PippidonRuleset.cs | 3 +++ 4 files changed, 12 insertions(+) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs index 00754c6346..1e88f87f09 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/EmptyFreeformRuleset.cs @@ -77,5 +77,8 @@ namespace osu.Game.Rulesets.EmptyFreeform }; } } + + // Leave this line intact. It will bake the correct version into the ruleset on each build/release. + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; } } diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs index 0522840e9e..2f6ba0dda6 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs @@ -49,5 +49,8 @@ namespace osu.Game.Rulesets.Pippidon }; public override Drawable CreateIcon() => new PippidonRulesetIcon(this); + + // Leave this line intact. It will bake the correct version into the ruleset on each build/release. + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; } } diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs index c8f0c07724..a32586c414 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/EmptyScrollingRuleset.cs @@ -54,5 +54,8 @@ namespace osu.Game.Rulesets.EmptyScrolling Text = ShortName[0].ToString(), Font = OsuFont.Default.With(size: 18), }; + + // Leave this line intact. It will bake the correct version into the ruleset on each build/release. + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; } } diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs index 89246373ee..bde530feb8 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs @@ -46,5 +46,8 @@ namespace osu.Game.Rulesets.Pippidon }; public override Drawable CreateIcon() => new PippidonRulesetIcon(this); + + // Leave this line intact. It will bake the correct version into the ruleset on each build/release. + public override string RulesetAPIVersionSupported => CURRENT_RULESET_API_VERSION; } } From 2dafa041a7e0a732159691f2107977555378e836 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 16:42:50 +0900 Subject: [PATCH 411/532] Account for offset being applied to editor clock time in `TestSceneEditorClock` --- osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs | 2 +- osu.Game/Beatmaps/FramedBeatmapClock.cs | 4 ++-- osu.Game/Screens/Edit/EditorClock.cs | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs index d598ebafa9..319f8ab9dc 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("seek near end", () => EditorClock.Seek(EditorClock.TrackLength - 250)); AddUntilStep("clock stops", () => !EditorClock.IsRunning); - AddUntilStep("clock stopped at end", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength)); + AddUntilStep("clock stopped at end", () => EditorClock.CurrentTime - EditorClock.TotalAppliedOffset, () => Is.EqualTo(EditorClock.TrackLength)); AddStep("start clock again", () => EditorClock.Start()); AddAssert("clock looped to start", () => EditorClock.IsRunning && EditorClock.CurrentTime < 500); diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index c86f25640f..a4787a34e8 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -124,7 +124,7 @@ namespace osu.Game.Beatmaps finalClockSource.ProcessFrame(); } - private double totalAppliedOffset + public double TotalAppliedOffset { get { @@ -169,7 +169,7 @@ namespace osu.Game.Beatmaps public bool Seek(double position) { - bool success = decoupledClock.Seek(position - totalAppliedOffset); + bool success = decoupledClock.Seek(position - TotalAppliedOffset); finalClockSource.ProcessFrame(); return success; diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 7a00a74530..6485f683ad 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -158,6 +158,8 @@ namespace osu.Game.Screens.Edit public double CurrentTime => underlyingClock.CurrentTime; + public double TotalAppliedOffset => underlyingClock.TotalAppliedOffset; + public void Reset() { ClearTransforms(); From 780121eeee359a433aba22dba057fcc2a55ef8d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 17:12:09 +0900 Subject: [PATCH 412/532] Add setting to toggle metronome in "Target" mod As mentioned in https://github.com/ppy/osu/discussions/20006#discussioncomment-3496732. --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 82260db818..b48f0b4ccd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -59,6 +59,9 @@ namespace osu.Game.Rulesets.Osu.Mods Value = null }; + [SettingSource("Metronome ticks", "Whether a metronome beat should play in the background")] + public BindableBool Metronome { get; } = new BindableBool(true); + #region Constants /// @@ -337,7 +340,8 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - drawableRuleset.Overlays.Add(new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime)); + if (Metronome.Value) + drawableRuleset.Overlays.Add(new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime)); } #endregion From 07b502f69af5bc73b84ac0df689fec121edc39f7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 29 Aug 2022 17:58:57 +0900 Subject: [PATCH 413/532] Simplify OrderByTotalScore implementation --- osu.Game/Scoring/ScoreManager.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index fd600f4864..782590114f 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -57,12 +57,7 @@ namespace osu.Game.Scoring /// The array of s to reorder. /// The given ordered by decreasing total score. public IEnumerable OrderByTotalScore(IEnumerable scores) - { - return scores.Select((score, index) => (score, totalScore: GetTotalScore(score))) - .OrderByDescending(g => g.totalScore) - .ThenBy(g => g.score.OnlineID) - .Select(g => g.score); - } + => scores.OrderByDescending(s => GetTotalScore(s)).ThenBy(s => s.OnlineID); /// /// Retrieves a bindable that represents the total score of a . From 3eda284b03b5abcabe78b03d743078313f8f52a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 18:17:41 +0900 Subject: [PATCH 414/532] Always reprocess beatmaps after a user update request This covers the rare case where metadata may have changed server-side but not the beatmap itself. Tested with the provided user database to resolve the issue. Closes #19976. --- osu.Game/Beatmaps/BeatmapImporter.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 0fa30cf5e7..292caa4397 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -55,7 +55,14 @@ namespace osu.Game.Beatmaps // If there were no changes, ensure we don't accidentally nuke ourselves. if (first.ID == original.ID) + { + first.PerformRead(s => + { + // Re-run processing even in this case. We might have outdated metadata. + ProcessBeatmap?.Invoke((s, false)); + }); return first; + } first.PerformWrite(updated => { From ad5ef529227d3687e8dd94573ea62b9e45b24f5d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 19:02:01 +0900 Subject: [PATCH 415/532] Add test coverage of resuming after pause not skipping forward in time --- .../Visual/Gameplay/TestScenePause.cs | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index cad8c62233..61b59747ea 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -90,6 +90,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("player not playing", () => !Player.LocalUserPlaying.Value); resumeAndConfirm(); + + AddAssert("Resumed without seeking forward", () => Player.LastResumeTime, () => Is.LessThanOrEqualTo(Player.LastPauseTime)); + AddUntilStep("player playing", () => Player.LocalUserPlaying.Value); } @@ -378,7 +381,16 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("pause overlay " + (isShown ? "shown" : "hidden"), () => Player.PauseOverlayVisible == isShown); private void confirmClockRunning(bool isRunning) => - AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.IsRunning == isRunning); + AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () => + { + bool completed = Player.GameplayClockContainer.IsRunning == isRunning; + + if (completed) + { + } + + return completed; + }); protected override bool AllowFail => true; @@ -386,6 +398,9 @@ namespace osu.Game.Tests.Visual.Gameplay protected class PausePlayer : TestPlayer { + public double LastPauseTime { get; private set; } + public double LastResumeTime { get; private set; } + public bool FailOverlayVisible => FailOverlay.State.Value == Visibility.Visible; public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible; @@ -399,6 +414,23 @@ namespace osu.Game.Tests.Visual.Gameplay base.OnEntering(e); GameplayClockContainer.Stop(); } + + private bool? isRunning; + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (GameplayClockContainer.IsRunning != isRunning) + { + isRunning = GameplayClockContainer.IsRunning; + + if (isRunning.Value) + LastResumeTime = GameplayClockContainer.CurrentTime; + else + LastPauseTime = GameplayClockContainer.CurrentTime; + } + } } } } From 75531d2d62d3f7e9f7c2fd04dd5ba53d884bcefc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 19:51:16 +0900 Subject: [PATCH 416/532] Fix gameplay skipping forward during resume operation --- .../Screens/Play/GameplayClockContainer.cs | 17 +++++-- .../Play/MasterGameplayClockContainer.cs | 45 ++++++++++++++++++- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 1ae393d06a..ff82fb96ec 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -88,9 +88,7 @@ namespace osu.Game.Screens.Play ensureSourceClockSet(); - // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time - // This accounts for the clock source potentially taking time to enter a completely stopped state - Seek(GameplayClock.CurrentTime); + PrepareStart(); // The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time. // Because we generally update our own current time quicker than children can query it (via Start/Seek/Update), @@ -111,11 +109,22 @@ namespace osu.Game.Screens.Play }); } + /// + /// When is called, this will be run to give an opportunity to prepare the clock at the correct + /// start location. + /// + protected virtual void PrepareStart() + { + // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time + // This accounts for the clock source potentially taking time to enter a completely stopped state + Seek(GameplayClock.CurrentTime); + } + /// /// Seek to a specific time in gameplay. /// /// The destination time to seek to. - public void Seek(double time) + public virtual void Seek(double time) { Logger.Log($"{nameof(GameplayClockContainer)} seeking to {time}"); diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 238817ad05..f0f5daf64d 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -8,6 +8,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Logging; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -45,6 +46,17 @@ namespace osu.Game.Screens.Play private readonly List> nonGameplayAdjustments = new List>(); + /// + /// Stores the time at which the last call was triggered. + /// This is used to ensure we resume from that precise point in time, ignoring the proceeding frequency ramp. + /// + /// Optimally, we'd have gameplay ramp down with the frequency, but I believe this was intentionally disabled + /// to avoid fails occurring after the pause screen has been shown. + /// + /// In the future I want to change this. + /// + private double? actualStopTime; + public override IEnumerable NonGameplayAdjustments => nonGameplayAdjustments.Select(b => b.Value); /// @@ -86,10 +98,12 @@ namespace osu.Game.Screens.Play protected override void StopGameplayClock() { + actualStopTime = GameplayClock.CurrentTime; + if (IsLoaded) { // During normal operation, the source is stopped after performing a frequency ramp. - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 200, Easing.Out).OnComplete(_ => + this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 2000, Easing.Out).OnComplete(_ => { if (IsPaused.Value) base.StopGameplayClock(); @@ -108,6 +122,25 @@ namespace osu.Game.Screens.Play } } + public override void Seek(double time) + { + // Safety in case the clock is seeked while stopped. + actualStopTime = null; + + base.Seek(time); + } + + protected override void PrepareStart() + { + if (actualStopTime != null) + { + Seek(actualStopTime.Value); + actualStopTime = null; + } + else + base.PrepareStart(); + } + protected override void StartGameplayClock() { addSourceClockAdjustments(); @@ -116,7 +149,7 @@ namespace osu.Game.Screens.Play if (IsLoaded) { - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 200, Easing.In); + this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 2000, Easing.In); } else { @@ -158,6 +191,14 @@ namespace osu.Game.Screens.Play private bool speedAdjustmentsApplied; + protected override void Update() + { + base.Update(); + + if (GameplayClock.ExternalPauseFrequencyAdjust.Value < 1) + Logger.Log($"{GameplayClock.CurrentTime}"); + } + private void addSourceClockAdjustments() { if (speedAdjustmentsApplied) From 1bff540381f8c7aaea4d4be68fab8bc69ffafa03 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 29 Aug 2022 22:04:37 +0900 Subject: [PATCH 417/532] Remove debug changes --- .../Screens/Play/MasterGameplayClockContainer.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index f0f5daf64d..2f1ffa126f 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -8,7 +8,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Logging; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -103,7 +102,7 @@ namespace osu.Game.Screens.Play if (IsLoaded) { // During normal operation, the source is stopped after performing a frequency ramp. - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 2000, Easing.Out).OnComplete(_ => + this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 0, 200, Easing.Out).OnComplete(_ => { if (IsPaused.Value) base.StopGameplayClock(); @@ -149,7 +148,7 @@ namespace osu.Game.Screens.Play if (IsLoaded) { - this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 2000, Easing.In); + this.TransformBindableTo(GameplayClock.ExternalPauseFrequencyAdjust, 1, 200, Easing.In); } else { @@ -191,14 +190,6 @@ namespace osu.Game.Screens.Play private bool speedAdjustmentsApplied; - protected override void Update() - { - base.Update(); - - if (GameplayClock.ExternalPauseFrequencyAdjust.Value < 1) - Logger.Log($"{GameplayClock.CurrentTime}"); - } - private void addSourceClockAdjustments() { if (speedAdjustmentsApplied) From a296c1ec81e6980927d8d8dc9338001758ae40e0 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 29 Aug 2022 16:05:35 +0200 Subject: [PATCH 418/532] remove call to changeHandler BeginChange --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 3d425afe9e..09f28b5b14 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -362,7 +362,6 @@ namespace osu.Game.Rulesets.Osu.Edit && Precision.AlmostBigger(1, Vector2.DistanceSquared(mergeableObjects[0].Position, mergeableObjects[1].Position)))) return; - ChangeHandler?.BeginChange(); EditorBeatmap.BeginChange(); // Have an initial slider object. @@ -440,7 +439,6 @@ namespace osu.Game.Rulesets.Osu.Edit SelectedItems.Add(mergedHitObject); EditorBeatmap.EndChange(); - ChangeHandler?.EndChange(); } protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) From 27ad224f13591fb35309d4612b074dd47b22f6cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 01:21:44 +0900 Subject: [PATCH 419/532] Remove probably unnecessary `Seek` on start --- osu.Game/Screens/Play/GameplayClockContainer.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index ff82fb96ec..6de88d7ad0 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -115,9 +115,6 @@ namespace osu.Game.Screens.Play /// protected virtual void PrepareStart() { - // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time - // This accounts for the clock source potentially taking time to enter a completely stopped state - Seek(GameplayClock.CurrentTime); } /// From 062a6fcc181da7e3ca597a9d6201491b16fc7898 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 01:21:53 +0900 Subject: [PATCH 420/532] Fix failing large offset test If we are going to continue to let the underlying clock process frames, there needs to be a bit of lenience to allow the backwards seek on resume (to play back over the freq ramp period). The test is meant to be ensuring we don't skip the full offset amount, so div10 seems pretty safe. --- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 61b59747ea..a6abdd7ee1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay Player.OnUpdate += _ => { double currentTime = Player.GameplayClockContainer.CurrentTime; - alwaysGoingForward &= currentTime >= lastTime; + alwaysGoingForward &= currentTime >= lastTime - 500; lastTime = currentTime; }; }); @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay resumeAndConfirm(); - AddAssert("time didn't go backwards", () => alwaysGoingForward); + AddAssert("time didn't go too far backwards", () => alwaysGoingForward); AddStep("reset offset", () => LocalConfig.SetValue(OsuSetting.AudioOffset, 0.0)); } From d50e9caa11a5930d49b39aa4ce18e11827730f7f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 29 Aug 2022 18:58:29 +0200 Subject: [PATCH 421/532] Moved guards to separate canMerge method --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 8b67c0dcc9..dd9d1614b8 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -354,12 +354,14 @@ namespace osu.Game.Rulesets.Osu.Edit .OrderBy(h => h.StartTime) .ToArray(); + private bool canMerge(IReadOnlyList objects) => + objects.Count > 1 && (objects.Any(h => h is Slider) || Precision.DefinitelyBigger(Vector2.DistanceSquared(objects[0].Position, objects[1].Position), 1)); + private void mergeSelection() { var mergeableObjects = selectedMergeableObjects; - if (mergeableObjects.Length < 2 || (mergeableObjects.All(h => h is not Slider) - && Precision.AlmostBigger(1, Vector2.DistanceSquared(mergeableObjects[0].Position, mergeableObjects[1].Position)))) + if (!canMerge(mergeableObjects)) return; ChangeHandler?.BeginChange(); @@ -446,9 +448,7 @@ namespace osu.Game.Rulesets.Osu.Edit foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; - var mergeableObjects = selectedMergeableObjects; - if (mergeableObjects.Length > 1 && (mergeableObjects.Any(h => h is Slider) - || Precision.DefinitelyBigger(Vector2.DistanceSquared(mergeableObjects[0].Position, mergeableObjects[1].Position), 1))) + if (canMerge(selectedMergeableObjects)) yield return new OsuMenuItem("Merge selection", MenuItemType.Destructive, mergeSelection); } } From 2e5770be4e14623f61327063f62019112d15e316 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 02:51:42 +0900 Subject: [PATCH 422/532] Move helper method to bottom of class --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index dd9d1614b8..048fd4ed7e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -354,9 +354,6 @@ namespace osu.Game.Rulesets.Osu.Edit .OrderBy(h => h.StartTime) .ToArray(); - private bool canMerge(IReadOnlyList objects) => - objects.Count > 1 && (objects.Any(h => h is Slider) || Precision.DefinitelyBigger(Vector2.DistanceSquared(objects[0].Position, objects[1].Position), 1)); - private void mergeSelection() { var mergeableObjects = selectedMergeableObjects; @@ -451,5 +448,10 @@ namespace osu.Game.Rulesets.Osu.Edit if (canMerge(selectedMergeableObjects)) yield return new OsuMenuItem("Merge selection", MenuItemType.Destructive, mergeSelection); } + + private bool canMerge(IReadOnlyList objects) => + objects.Count > 1 + && (objects.Any(h => h is Slider) + || Precision.DefinitelyBigger(Vector2.DistanceSquared(objects[0].Position, objects[1].Position), 1)); } } From 44916c51d73580ab42c5a18f33705da9ffae9a16 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 30 Aug 2022 00:18:55 +0200 Subject: [PATCH 423/532] Updated canMerge check to be totally accurate --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 048fd4ed7e..ac5fad54a4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -452,6 +452,6 @@ namespace osu.Game.Rulesets.Osu.Edit private bool canMerge(IReadOnlyList objects) => objects.Count > 1 && (objects.Any(h => h is Slider) - || Precision.DefinitelyBigger(Vector2.DistanceSquared(objects[0].Position, objects[1].Position), 1)); + || objects.Zip(objects.Skip(1), (h1, h2) => Precision.DefinitelyBigger(Vector2.DistanceSquared(h1.Position, h2.Position), 1)).Any(x => x)); } } From 5d41fdfc890acd4d9dc1d45e3e96f890ec12f1b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 15:05:21 +0900 Subject: [PATCH 424/532] Remove unnecessary usage of `DrawableAudioMixer` in `ScorePanel` --- osu.Game/Screens/Ranking/ScorePanel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 0bcfa0da1f..cb777de144 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -99,7 +99,7 @@ namespace osu.Game.Screens.Ranking [Resolved] private OsuGameBase game { get; set; } - private DrawableAudioMixer mixer; + private AudioContainer audioContent; private bool displayWithFlair; @@ -130,7 +130,7 @@ namespace osu.Game.Screens.Ranking // Adding a manual offset here allows the expanded version to take on an "acceptable" vertical centre when at 100% UI scale. const float vertical_fudge = 20; - InternalChild = mixer = new DrawableAudioMixer + InternalChild = audioContent = new AudioContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -225,7 +225,7 @@ namespace osu.Game.Screens.Ranking protected override void Update() { base.Update(); - mixer.Balance.Value = (ScreenSpaceDrawQuad.Centre.X / game.ScreenSpaceDrawQuad.Width) * 2 - 1; + audioContent.Balance.Value = (ScreenSpaceDrawQuad.Centre.X / game.ScreenSpaceDrawQuad.Width) * 2 - 1; } private void playAppearSample() @@ -274,7 +274,7 @@ namespace osu.Game.Screens.Ranking break; } - mixer.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint); + audioContent.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint); bool topLayerExpanded = topLayerContainer.Y < 0; From 5202c15a0e3edc10e3667805c0db15fe059e441b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 30 Aug 2022 15:11:39 +0900 Subject: [PATCH 425/532] Populate MaximumStatistics for test scores --- osu.Game.Tests/Resources/TestResources.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index 6bce03869d..9c85f61330 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -194,8 +194,16 @@ namespace osu.Game.Tests.Resources [HitResult.LargeTickHit] = 100, [HitResult.LargeTickMiss] = 50, [HitResult.SmallBonus] = 10, - [HitResult.SmallBonus] = 50 + [HitResult.LargeBonus] = 50 }, + MaximumStatistics = new Dictionary + { + [HitResult.Perfect] = 971, + [HitResult.SmallTickHit] = 75, + [HitResult.LargeTickHit] = 150, + [HitResult.SmallBonus] = 10, + [HitResult.LargeBonus] = 50, + } }; private class TestModHardRock : ModHardRock From 8b3742188fcb9c4c80c7515546091b2ec714ec3e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 30 Aug 2022 15:42:33 +0900 Subject: [PATCH 426/532] Fix test by also clearing out maximum statistics --- .../Visual/Playlists/TestScenePlaylistsResultsScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 8a04cd96fe..26fa740159 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -61,6 +61,7 @@ namespace osu.Game.Tests.Visual.Playlists userScore = TestResources.CreateTestScoreInfo(); userScore.TotalScore = 0; userScore.Statistics = new Dictionary(); + userScore.MaximumStatistics = new Dictionary(); bindHandler(); From 799c015bff82460ffb1c1392610d47984281a443 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 30 Aug 2022 15:50:19 +0900 Subject: [PATCH 427/532] Add LegacyTotalScore to SoloScoreInfo --- osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index 16aa800cb0..a8cedabd48 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -77,6 +77,12 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("maximum_statistics")] public Dictionary MaximumStatistics { get; set; } = new Dictionary(); + /// + /// Used to preserve the total score for legacy scores. + /// + [JsonProperty("legacy_total_score")] + public int? LegacyTotalScore { get; set; } + #region osu-web API additions (not stored to database). [JsonProperty("id")] From b8fda1a16f3668e244086ba5958f9b818c95c02b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 15:51:46 +0900 Subject: [PATCH 428/532] Apply NRT to notification classes and tidy things up a bit. --- .../TestSceneNotificationOverlay.cs | 10 +++--- .../Database/ImportProgressNotification.cs | 2 -- osu.Game/Overlays/NotificationOverlay.cs | 12 ++----- .../Overlays/Notifications/Notification.cs | 8 ++--- .../Notifications/NotificationSection.cs | 14 ++++---- .../Notifications/ProgressNotification.cs | 32 +++++++++---------- .../Notifications/SimpleNotification.cs | 2 -- 7 files changed, 32 insertions(+), 48 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index cf069a9e34..c196b204b9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -19,11 +17,11 @@ namespace osu.Game.Tests.Visual.UserInterface [TestFixture] public class TestSceneNotificationOverlay : OsuTestScene { - private NotificationOverlay notificationOverlay; + private NotificationOverlay notificationOverlay = null!; private readonly List progressingNotifications = new List(); - private SpriteText displayedCount; + private SpriteText displayedCount = null!; [SetUp] public void SetUp() => Schedule(() => @@ -46,7 +44,7 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestCompleteProgress() { - ProgressNotification notification = null; + ProgressNotification notification = null!; AddStep("add progress notification", () => { notification = new ProgressNotification @@ -64,7 +62,7 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestCancelProgress() { - ProgressNotification notification = null; + ProgressNotification notification = null!; AddStep("add progress notification", () => { notification = new ProgressNotification diff --git a/osu.Game/Database/ImportProgressNotification.cs b/osu.Game/Database/ImportProgressNotification.cs index 46f9936bc2..aaee3e117f 100644 --- a/osu.Game/Database/ImportProgressNotification.cs +++ b/osu.Game/Database/ImportProgressNotification.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Overlays.Notifications; namespace osu.Game.Database diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 281077bcf6..cbcc7b6886 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -64,14 +64,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Children = new[] { - new NotificationSection(AccountsStrings.NotificationsTitle, "Clear All") - { - AcceptTypes = new[] { typeof(SimpleNotification) } - }, - new NotificationSection(@"Running Tasks", @"Cancel All") - { - AcceptTypes = new[] { typeof(ProgressNotification) } - } + new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, "Clear All"), + new NotificationSection(@"Running Tasks", new[] { typeof(ProgressNotification) }, @"Cancel All"), } } } @@ -133,7 +127,7 @@ namespace osu.Game.Overlays var ourType = notification.GetType(); - var section = sections.Children.FirstOrDefault(s => s.AcceptTypes.Any(accept => accept.IsAssignableFrom(ourType))); + var section = sections.Children.FirstOrDefault(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); section?.Add(notification, notification.DisplayOnTop ? -runningDepth : runningDepth); if (notification.IsImportant) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 64bf3693c8..fbb906e637 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -26,7 +24,7 @@ namespace osu.Game.Overlays.Notifications /// /// User requested close. /// - public event Action Closed; + public event Action? Closed; public abstract LocalisableString Text { get; set; } @@ -38,7 +36,7 @@ namespace osu.Game.Overlays.Notifications /// /// Run on user activating the notification. Return true to close. /// - public Func Activated; + public Func? Activated; /// /// Should we show at the top of our section on display? @@ -212,7 +210,7 @@ namespace osu.Game.Overlays.Notifications public class NotificationLight : Container { private bool pulsate; - private Container pulsateLayer; + private Container pulsateLayer = null!; public bool Pulsate { diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index b7e21822fa..d2e18a0cee 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -21,9 +19,9 @@ namespace osu.Game.Overlays.Notifications { public class NotificationSection : AlwaysUpdateFillFlowContainer { - private OsuSpriteText countDrawable; + private OsuSpriteText countDrawable = null!; - private FlowContainer notifications; + private FlowContainer notifications = null!; public int DisplayedCount => notifications.Count(n => !n.WasClosed); public int UnreadCount => notifications.Count(n => !n.WasClosed && !n.Read); @@ -33,14 +31,16 @@ namespace osu.Game.Overlays.Notifications notifications.Insert((int)position, notification); } - public IEnumerable AcceptTypes; + public IEnumerable AcceptedNotificationTypes { get; } private readonly string clearButtonText; private readonly LocalisableString titleText; - public NotificationSection(LocalisableString title, string clearButtonText) + public NotificationSection(LocalisableString title, IEnumerable acceptedNotificationTypes, string clearButtonText) { + AcceptedNotificationTypes = acceptedNotificationTypes.ToArray(); + this.clearButtonText = clearButtonText.ToUpperInvariant(); titleText = title; } @@ -159,7 +159,7 @@ namespace osu.Game.Overlays.Notifications public void MarkAllRead() { - notifications?.Children.ForEach(n => n.Read = true); + notifications.Children.ForEach(n => n.Read = true); } } diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 719e77db83..15346930a3 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Threading; using osu.Framework.Allocation; @@ -25,6 +23,18 @@ namespace osu.Game.Overlays.Notifications { private const float loading_spinner_size = 22; + public Func? CancelRequested { get; set; } + + /// + /// The function to post completion notifications back to. + /// + public Action? CompletionTarget { get; set; } + + /// + /// An action to complete when the completion notification is clicked. Return true to close. + /// + public Func? CompletionClickAction { get; set; } + private LocalisableString text; public override LocalisableString Text @@ -142,7 +152,7 @@ namespace osu.Game.Overlays.Notifications Text = CompletionText }; - protected virtual void Completed() + protected void Completed() { CompletionTarget?.Invoke(CreateCompletionNotification()); base.Close(); @@ -155,8 +165,8 @@ namespace osu.Game.Overlays.Notifications private Color4 colourActive; private Color4 colourCancelled; - private Box iconBackground; - private LoadingSpinner loadingSpinner; + private Box iconBackground = null!; + private LoadingSpinner loadingSpinner = null!; private readonly TextFlowContainer textDrawable; @@ -222,18 +232,6 @@ namespace osu.Game.Overlays.Notifications } } - public Func CancelRequested { get; set; } - - /// - /// The function to post completion notifications back to. - /// - public Action CompletionTarget { get; set; } - - /// - /// An action to complete when the completion notification is clicked. Return true to close. - /// - public Func CompletionClickAction; - private class ProgressBar : Container { private readonly Box box; diff --git a/osu.Game/Overlays/Notifications/SimpleNotification.cs b/osu.Game/Overlays/Notifications/SimpleNotification.cs index fc594902cf..b9a1cc6d90 100644 --- a/osu.Game/Overlays/Notifications/SimpleNotification.cs +++ b/osu.Game/Overlays/Notifications/SimpleNotification.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; From 1484ae19f0a7a3f4a069bf8628dd5c8805d923db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 17:33:08 +0900 Subject: [PATCH 429/532] Initial design update pass --- osu.Game/Overlays/NotificationOverlay.cs | 6 +- .../Overlays/Notifications/Notification.cs | 147 ++++++++++-------- .../Notifications/ProgressNotification.cs | 3 +- .../Notifications/SimpleNotification.cs | 32 ++-- 4 files changed, 100 insertions(+), 88 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index cbcc7b6886..bd483e073c 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Framework.Threading; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays.Notifications; using osu.Game.Resources.Localisation.Web; @@ -35,6 +34,9 @@ namespace osu.Game.Overlays [Resolved] private AudioManager audio { get; set; } = null!; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + private readonly IBindable firstRunSetupVisibility = new Bindable(); [BackgroundDependencyLoader] @@ -49,7 +51,7 @@ namespace osu.Game.Overlays new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(0.05f), + Colour = colourProvider.Background4, }, new OsuScrollContainer { diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index fbb906e637..5cd0db628d 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; using osuTK.Graphics; @@ -46,8 +45,9 @@ namespace osu.Game.Overlays.Notifications public virtual string PopInSampleName => "UI/notification-pop-in"; protected NotificationLight Light; - private readonly CloseButton closeButton; + protected Container IconContent; + private readonly Container content; protected override Container Content => content; @@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Notifications RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - AddRangeInternal(new Drawable[] + InternalChildren = new Drawable[] { Light = new NotificationLight { @@ -79,64 +79,68 @@ namespace osu.Game.Overlays.Notifications AutoSizeEasing = Easing.OutQuint, Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - new Container + new GridContainer { RelativeSizeAxes = Axes.X, - Padding = new MarginPadding(5), AutoSizeAxes = Axes.Y, - Children = new Drawable[] + RowDimensions = new[] { - IconContent = new Container - { - Size = new Vector2(40), - Masking = true, - CornerRadius = 5, - }, - content = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Left = 45, - Right = 30 - }, - } - } - }, - closeButton = new CloseButton - { - Alpha = 0, - Action = Close, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Margin = new MarginPadding - { - Right = 5 + new Dimension(GridSizeMode.AutoSize, minSize: 60) }, - } + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + IconContent = new Container + { + Width = 40, + RelativeSizeAxes = Axes.Y, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10), + Children = new Drawable[] + { + content = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + } + }, + new CloseButton + { + Action = Close, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } + } + }, + }, } } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + NotificationContent.Add(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + Depth = float.MaxValue }); } - protected override bool OnHover(HoverEvent e) - { - closeButton.FadeIn(75); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - closeButton.FadeOut(75); - base.OnHoverLost(e); - } - protected override bool OnClick(ClickEvent e) { if (Activated?.Invoke() ?? true) @@ -150,6 +154,7 @@ namespace osu.Game.Overlays.Notifications base.LoadComplete(); this.FadeInFromZero(200); + NotificationContent.MoveToX(DrawSize.X); NotificationContent.MoveToX(0, 500, Easing.OutQuint); } @@ -169,40 +174,48 @@ namespace osu.Game.Overlays.Notifications private class CloseButton : OsuClickableContainer { - private Color4 hoverColour; + private SpriteIcon icon = null!; + private Box background = null!; - public CloseButton() + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() { - Colour = OsuColour.Gray(0.2f); - AutoSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.Y; + Width = 24; - Children = new[] + Children = new Drawable[] { - new SpriteIcon + background = new Box + { + Colour = colourProvider.Background4, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + icon = new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Icon = FontAwesome.Solid.TimesCircle, - Size = new Vector2(20), + Icon = FontAwesome.Solid.Check, + Size = new Vector2(12), + Colour = colourProvider.Foreground1, } }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - hoverColour = colours.Yellow; - } - protected override bool OnHover(HoverEvent e) { - this.FadeColour(hoverColour, 200); + background.FadeIn(200); + icon.FadeColour(colourProvider.Content1, 200); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - this.FadeColour(OsuColour.Gray(0.2f), 200); + background.FadeOut(200); + icon.FadeColour(colourProvider.Foreground1, 200); base.OnHoverLost(e); } } diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 15346930a3..6390d9a54f 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -57,7 +57,7 @@ namespace osu.Game.Overlays.Notifications set { progress = value; - Scheduler.AddOnce(updateProgress, progress); + Scheduler.AddOnce(p => progressBar.Progress = p, progress); } } @@ -174,7 +174,6 @@ namespace osu.Game.Overlays.Notifications { Content.Add(textDrawable = new OsuTextFlowContainer { - Colour = OsuColour.Gray(128), AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, }); diff --git a/osu.Game/Overlays/Notifications/SimpleNotification.cs b/osu.Game/Overlays/Notifications/SimpleNotification.cs index b9a1cc6d90..ae4d183258 100644 --- a/osu.Game/Overlays/Notifications/SimpleNotification.cs +++ b/osu.Game/Overlays/Notifications/SimpleNotification.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; @@ -24,7 +23,8 @@ namespace osu.Game.Overlays.Notifications set { text = value; - textDrawable.Text = text; + if (textDrawable != null) + textDrawable.Text = text; } } @@ -36,48 +36,46 @@ namespace osu.Game.Overlays.Notifications set { icon = value; - iconDrawable.Icon = icon; + if (iconDrawable != null) + iconDrawable.Icon = icon; } } - private readonly TextFlowContainer textDrawable; - private readonly SpriteIcon iconDrawable; + protected Box IconBackground = null!; - protected Box IconBackground; + private TextFlowContainer? textDrawable; - public SimpleNotification() + private SpriteIcon? iconDrawable; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, OverlayColourProvider colourProvider) { + Light.Colour = colours.Green; + IconContent.AddRange(new Drawable[] { IconBackground = new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.2f), OsuColour.Gray(0.6f)) + Colour = colourProvider.Background5, }, iconDrawable = new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = icon, - Size = new Vector2(20), + Size = new Vector2(16), } }); - Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14)) + Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium)) { - Colour = OsuColour.Gray(128), AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Text = text }); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Light.Colour = colours.Green; - } - public override bool Read { get => base.Read; From 0f203531d938fc39e0b8c99274cf35ccf844b5a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 17:37:43 +0900 Subject: [PATCH 430/532] Allow customising the "close" button icon --- osu.Game/Overlays/Notifications/Notification.cs | 13 +++++++++++-- .../Overlays/Notifications/ProgressNotification.cs | 5 ++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 5cd0db628d..2b57423305 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -56,6 +56,8 @@ namespace osu.Game.Overlays.Notifications public virtual bool Read { get; set; } + protected virtual IconUsage CloseButtonIcon => FontAwesome.Solid.Check; + protected Notification() { RelativeSizeAxes = Axes.X; @@ -116,7 +118,7 @@ namespace osu.Game.Overlays.Notifications }, } }, - new CloseButton + new CloseButton(CloseButtonIcon) { Action = Close, Anchor = Anchor.TopRight, @@ -177,9 +179,16 @@ namespace osu.Game.Overlays.Notifications private SpriteIcon icon = null!; private Box background = null!; + private readonly IconUsage iconUsage; + [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + public CloseButton(IconUsage iconUsage) + { + this.iconUsage = iconUsage; + } + [BackgroundDependencyLoader] private void load() { @@ -198,7 +207,7 @@ namespace osu.Game.Overlays.Notifications { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Icon = FontAwesome.Solid.Check, + Icon = iconUsage, Size = new Vector2(12), Colour = colourProvider.Foreground1, } diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 6390d9a54f..594537bce7 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -61,7 +61,10 @@ namespace osu.Game.Overlays.Notifications } } - private void updateProgress(float progress) => progressBar.Progress = progress; + protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; protected override void LoadComplete() { From 09aa3e065d790ad2f1cb44024dc7b06bd9cc8b73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 17:40:35 +0900 Subject: [PATCH 431/532] Move colouring to full icon content rather than background --- osu.Desktop/Security/ElevatedPrivilegesChecker.cs | 2 +- osu.Game/Database/TooManyDownloadsNotification.cs | 2 +- osu.Game/Online/Chat/MessageNotifier.cs | 2 +- .../Notifications/ProgressCompletionNotification.cs | 2 +- .../Overlays/Notifications/ProgressNotification.cs | 11 +++++------ osu.Game/Overlays/Notifications/SimpleNotification.cs | 4 +--- osu.Game/Screens/Play/PlayerLoader.cs | 4 ++-- osu.Game/Updater/UpdateManager.cs | 2 +- 8 files changed, 13 insertions(+), 16 deletions(-) diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs index 9959b24b35..cc4337fb02 100644 --- a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs +++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs @@ -76,7 +76,7 @@ namespace osu.Desktop.Security private void load(OsuColour colours) { Icon = FontAwesome.Solid.ShieldAlt; - IconBackground.Colour = colours.YellowDark; + IconContent.Colour = colours.YellowDark; } } } diff --git a/osu.Game/Database/TooManyDownloadsNotification.cs b/osu.Game/Database/TooManyDownloadsNotification.cs index aa88fed43c..14012e1d34 100644 --- a/osu.Game/Database/TooManyDownloadsNotification.cs +++ b/osu.Game/Database/TooManyDownloadsNotification.cs @@ -20,7 +20,7 @@ namespace osu.Game.Database [BackgroundDependencyLoader] private void load(OsuColour colours) { - IconBackground.Colour = colours.RedDark; + IconContent.Colour = colours.RedDark; } } } diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 3fc6a008e8..22c2b4690e 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -174,7 +174,7 @@ namespace osu.Game.Online.Chat [BackgroundDependencyLoader] private void load(OsuColour colours, ChatOverlay chatOverlay, INotificationOverlay notificationOverlay) { - IconBackground.Colour = colours.PurpleDark; + IconContent.Colour = colours.PurpleDark; Activated = delegate { diff --git a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs index cb9d54c14c..49d558285c 100644 --- a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Notifications [BackgroundDependencyLoader] private void load(OsuColour colours) { - IconBackground.Colour = ColourInfo.GradientVertical(colours.GreenDark, colours.GreenLight); + IconContent.Colour = ColourInfo.GradientVertical(colours.GreenDark, colours.GreenLight); } } } diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 594537bce7..c0342f1c2a 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -103,7 +103,7 @@ namespace osu.Game.Overlays.Notifications Light.Pulsate = false; progressBar.Active = false; - iconBackground.FadeColour(ColourInfo.GradientVertical(colourQueued, colourQueued.Lighten(0.5f)), colour_fade_duration); + IconContent.FadeColour(ColourInfo.GradientVertical(colourQueued, colourQueued.Lighten(0.5f)), colour_fade_duration); loadingSpinner.Show(); break; @@ -112,14 +112,14 @@ namespace osu.Game.Overlays.Notifications Light.Pulsate = true; progressBar.Active = true; - iconBackground.FadeColour(ColourInfo.GradientVertical(colourActive, colourActive.Lighten(0.5f)), colour_fade_duration); + IconContent.FadeColour(ColourInfo.GradientVertical(colourActive, colourActive.Lighten(0.5f)), colour_fade_duration); loadingSpinner.Show(); break; case ProgressNotificationState.Cancelled: cancellationTokenSource.Cancel(); - iconBackground.FadeColour(ColourInfo.GradientVertical(Color4.Gray, Color4.Gray.Lighten(0.5f)), colour_fade_duration); + IconContent.FadeColour(ColourInfo.GradientVertical(Color4.Gray, Color4.Gray.Lighten(0.5f)), colour_fade_duration); loadingSpinner.Hide(); var icon = new SpriteIcon @@ -168,7 +168,6 @@ namespace osu.Game.Overlays.Notifications private Color4 colourActive; private Color4 colourCancelled; - private Box iconBackground = null!; private LoadingSpinner loadingSpinner = null!; private readonly TextFlowContainer textDrawable; @@ -206,10 +205,10 @@ namespace osu.Game.Overlays.Notifications IconContent.AddRange(new Drawable[] { - iconBackground = new Box + new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.White, + Colour = colourProvider.Background5, }, loadingSpinner = new LoadingSpinner { diff --git a/osu.Game/Overlays/Notifications/SimpleNotification.cs b/osu.Game/Overlays/Notifications/SimpleNotification.cs index ae4d183258..1dba60fb5f 100644 --- a/osu.Game/Overlays/Notifications/SimpleNotification.cs +++ b/osu.Game/Overlays/Notifications/SimpleNotification.cs @@ -41,8 +41,6 @@ namespace osu.Game.Overlays.Notifications } } - protected Box IconBackground = null!; - private TextFlowContainer? textDrawable; private SpriteIcon? iconDrawable; @@ -54,7 +52,7 @@ namespace osu.Game.Overlays.Notifications IconContent.AddRange(new Drawable[] { - IconBackground = new Box + new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background5, diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index e6bd1367ef..a9fcab063c 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -530,7 +530,7 @@ namespace osu.Game.Screens.Play private void load(OsuColour colours, AudioManager audioManager, INotificationOverlay notificationOverlay, VolumeOverlay volumeOverlay) { Icon = FontAwesome.Solid.VolumeMute; - IconBackground.Colour = colours.RedDark; + IconContent.Colour = colours.RedDark; Activated = delegate { @@ -584,7 +584,7 @@ namespace osu.Game.Screens.Play private void load(OsuColour colours, INotificationOverlay notificationOverlay) { Icon = FontAwesome.Solid.BatteryQuarter; - IconBackground.Colour = colours.RedDark; + IconContent.Colour = colours.RedDark; Activated = delegate { diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index d60f2e4a4b..4790055cd1 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -99,7 +99,7 @@ namespace osu.Game.Updater private void load(OsuColour colours, ChangelogOverlay changelog, INotificationOverlay notificationOverlay) { Icon = FontAwesome.Solid.CheckSquare; - IconBackground.Colour = colours.BlueDark; + IconContent.Colour = colours.BlueDark; Activated = delegate { From bea12ab3c262bb777dbba82b4fbc74ce1dd38c60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 17:49:29 +0900 Subject: [PATCH 432/532] Rename `NotificationContent` to `MainContent` --- osu.Game/Overlays/Notifications/Notification.cs | 10 +++++----- .../Overlays/Notifications/ProgressNotification.cs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 2b57423305..b04e732f92 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -52,7 +52,7 @@ namespace osu.Game.Overlays.Notifications protected override Container Content => content; - protected Container NotificationContent; + protected Container MainContent; public virtual bool Read { get; set; } @@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Notifications Anchor = Anchor.CentreLeft, Origin = Anchor.CentreRight, }, - NotificationContent = new Container + MainContent = new Container { CornerRadius = 8, Masking = true, @@ -135,7 +135,7 @@ namespace osu.Game.Overlays.Notifications [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - NotificationContent.Add(new Box + MainContent.Add(new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background3, @@ -157,8 +157,8 @@ namespace osu.Game.Overlays.Notifications this.FadeInFromZero(200); - NotificationContent.MoveToX(DrawSize.X); - NotificationContent.MoveToX(0, 500, Easing.OutQuint); + MainContent.MoveToX(DrawSize.X); + MainContent.MoveToX(0, 500, Easing.OutQuint); } public bool WasClosed; diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index c0342f1c2a..2f813b4ad5 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Notifications case ProgressNotificationState.Completed: loadingSpinner.Hide(); - NotificationContent.MoveToY(-DrawSize.Y / 2, 200, Easing.OutQuint); + MainContent.MoveToY(-DrawSize.Y / 2, 200, Easing.OutQuint); this.FadeOut(200).Finally(_ => Completed()); break; } @@ -180,7 +180,7 @@ namespace osu.Game.Overlays.Notifications RelativeSizeAxes = Axes.X, }); - NotificationContent.Add(progressBar = new ProgressBar + MainContent.Add(progressBar = new ProgressBar { Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, From c846bf20a76ffb9adf5c698f64f3c668b04f90aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 17:58:32 +0900 Subject: [PATCH 433/532] Add background hover and adjust remaining metrics --- .../Overlays/Notifications/Notification.cs | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index b04e732f92..a759e2efd1 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -58,6 +58,11 @@ namespace osu.Game.Overlays.Notifications protected virtual IconUsage CloseButtonIcon => FontAwesome.Solid.Check; + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Box background = null!; + protected Notification() { RelativeSizeAxes = Axes.X; @@ -73,7 +78,7 @@ namespace osu.Game.Overlays.Notifications }, MainContent = new Container { - CornerRadius = 8, + CornerRadius = 6, Masking = true, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -133,9 +138,9 @@ namespace osu.Game.Overlays.Notifications } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load() { - MainContent.Add(new Box + MainContent.Add(background = new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background3, @@ -143,6 +148,18 @@ namespace osu.Game.Overlays.Notifications }); } + protected override bool OnHover(HoverEvent e) + { + background.FadeColour(colourProvider.Background2, 200, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + background.FadeColour(colourProvider.Background3, 200, Easing.OutQuint); + base.OnHoverLost(e); + } + protected override bool OnClick(ClickEvent e) { if (Activated?.Invoke() ?? true) @@ -193,7 +210,7 @@ namespace osu.Game.Overlays.Notifications private void load() { RelativeSizeAxes = Axes.Y; - Width = 24; + Width = 28; Children = new Drawable[] { @@ -216,15 +233,15 @@ namespace osu.Game.Overlays.Notifications protected override bool OnHover(HoverEvent e) { - background.FadeIn(200); - icon.FadeColour(colourProvider.Content1, 200); + background.FadeIn(200, Easing.OutQuint); + icon.FadeColour(colourProvider.Content1, 200, Easing.OutQuint); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - background.FadeOut(200); - icon.FadeColour(colourProvider.Foreground1, 200); + background.FadeOut(200, Easing.OutQuint); + icon.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint); base.OnHoverLost(e); } } From d600058c98419657c7f4b9d630b34ff8b5d2a613 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 18:09:10 +0900 Subject: [PATCH 434/532] Assert non-null in `ProfileHeader` to appease r# --- osu.Game/Overlays/Profile/ProfileHeader.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index f84f829f7a..1eca6a81cf 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -37,6 +38,10 @@ namespace osu.Game.Overlays.Profile // todo: pending implementation. // TabControl.AddItem(LayoutStrings.HeaderUsersModding); + // Haphazardly guaranteed by OverlayHeader constructor (see CreateBackground / CreateContent). + Debug.Assert(centreHeaderContainer != null); + Debug.Assert(detailHeaderContainer != null); + centreHeaderContainer.DetailsVisible.BindValueChanged(visible => detailHeaderContainer.Expanded = visible.NewValue, true); } From 928bce8fcdee3daf785539cd068b48274eae30e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 18:18:46 +0900 Subject: [PATCH 435/532] Fix crash when attempting to watch a replay when the storage file doesn't exist --- osu.Game/Scoring/LegacyDatabasedScore.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Scoring/LegacyDatabasedScore.cs b/osu.Game/Scoring/LegacyDatabasedScore.cs index de35cb5faf..a7641c7999 100644 --- a/osu.Game/Scoring/LegacyDatabasedScore.cs +++ b/osu.Game/Scoring/LegacyDatabasedScore.cs @@ -25,7 +25,12 @@ namespace osu.Game.Scoring return; using (var stream = store.GetStream(replayFilename)) + { + if (stream == null) + return; + Replay = new DatabasedLegacyScoreDecoder(rulesets, beatmaps).Parse(stream).Replay; + } } } } From 6b71b4656d6a10e085d9ab9119110293c7250cbd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 19:16:59 +0900 Subject: [PATCH 436/532] Remove `ProgressNotification` vertical movement and delay --- osu.Game/Overlays/Notifications/ProgressNotification.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 2f813b4ad5..36b8bac873 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -141,8 +141,7 @@ namespace osu.Game.Overlays.Notifications case ProgressNotificationState.Completed: loadingSpinner.Hide(); - MainContent.MoveToY(-DrawSize.Y / 2, 200, Easing.OutQuint); - this.FadeOut(200).Finally(_ => Completed()); + Completed(); break; } } From 60413e3e7bac003d6d7ceab82b704d4efd236bb5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 19:17:40 +0900 Subject: [PATCH 437/532] Enable masking for main content to avoid underlap with close button on word wrap failure. --- osu.Game/Overlays/Notifications/Notification.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index a759e2efd1..90d9b88b79 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -118,6 +118,7 @@ namespace osu.Game.Overlays.Notifications { content = new Container { + Masking = true, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, }, From 7b006f1f2249278d67a073bbd747e0957aede85a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 20:34:27 +0900 Subject: [PATCH 438/532] Add flash when a new notification is displayed to draw attention --- osu.Game/Overlays/Notifications/Notification.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 90d9b88b79..3407995502 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -61,6 +61,8 @@ namespace osu.Game.Overlays.Notifications [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + private readonly Box initialFlash; + private Box background = null!; protected Notification() @@ -133,6 +135,12 @@ namespace osu.Game.Overlays.Notifications } }, }, + initialFlash = new Box + { + Colour = Color4.White.Opacity(0.8f), + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + }, } } }; @@ -177,6 +185,8 @@ namespace osu.Game.Overlays.Notifications MainContent.MoveToX(DrawSize.X); MainContent.MoveToX(0, 500, Easing.OutQuint); + + initialFlash.FadeOutFromOne(2000, Easing.OutQuart); } public bool WasClosed; From b8300ae60a29216e63790857e486614f3c295eac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 20:57:12 +0900 Subject: [PATCH 439/532] Add toast notification tray --- osu.Game/Overlays/NotificationOverlay.cs | 81 +++++++++--- .../Overlays/NotificationOverlayToastTray.cs | 124 ++++++++++++++++++ 2 files changed, 184 insertions(+), 21 deletions(-) create mode 100644 osu.Game/Overlays/NotificationOverlayToastTray.cs diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index bd483e073c..096de558dd 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -15,6 +15,7 @@ using osu.Framework.Threading; using osu.Game.Graphics.Containers; using osu.Game.Overlays.Notifications; using osu.Game.Resources.Localisation.Web; +using osuTK; using NotificationsStrings = osu.Game.Localisation.NotificationsStrings; namespace osu.Game.Overlays @@ -39,6 +40,23 @@ namespace osu.Game.Overlays private readonly IBindable firstRunSetupVisibility = new Bindable(); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + if (State.Value == Visibility.Visible) + return base.ReceivePositionalInputAt(screenSpacePos); + + if (toastTray.IsDisplayingToasts) + return toastTray.ReceivePositionalInputAt(screenSpacePos); + + return false; + } + + public override bool PropagatePositionalInputSubTree => base.PropagatePositionalInputSubTree || toastTray.IsDisplayingToasts; + + private NotificationOverlayToastTray toastTray = null!; + + private Container mainContent = null!; + [BackgroundDependencyLoader] private void load(FirstRunSetupOverlay? firstRunSetup) { @@ -48,30 +66,41 @@ namespace osu.Game.Overlays Children = new Drawable[] { - new Box + toastTray = new NotificationOverlayToastTray { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4, + Origin = Anchor.TopRight, }, - new OsuScrollContainer + mainContent = new Container { - Masking = true, RelativeSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { - sections = new FillFlowContainer + new Box { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + new OsuScrollContainer + { + Masking = true, + RelativeSizeAxes = Axes.Both, Children = new[] { - new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, "Clear All"), - new NotificationSection(@"Running Tasks", new[] { typeof(ProgressNotification) }, @"Cancel All"), + sections = new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Children = new[] + { + new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }, "Clear All"), + new NotificationSection(@"Running Tasks", new[] { typeof(ProgressNotification) }, @"Cancel All"), + } + } } } } - } + }, }; if (firstRunSetup != null) @@ -129,14 +158,22 @@ namespace osu.Game.Overlays var ourType = notification.GetType(); - var section = sections.Children.FirstOrDefault(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); - section?.Add(notification, notification.DisplayOnTop ? -runningDepth : runningDepth); + int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; - if (notification.IsImportant) - Show(); + if (State.Value == Visibility.Hidden) + toastTray.Post(notification, addPermanently); + else + addPermanently(); - updateCounts(); - playDebouncedSample(notification.PopInSampleName); + void addPermanently() + { + var section = sections.Children.First(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); + + section.Add(notification, depth); + + updateCounts(); + playDebouncedSample(notification.PopInSampleName); + } }); protected override void Update() @@ -152,7 +189,9 @@ namespace osu.Game.Overlays base.PopIn(); this.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint); - this.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); + mainContent.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint); + + toastTray.FlushAllToasts(); } protected override void PopOut() @@ -162,7 +201,7 @@ namespace osu.Game.Overlays markAllRead(); this.MoveToX(WIDTH, TRANSITION_LENGTH, Easing.OutQuint); - this.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint); + mainContent.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint); } private void notificationClosed() diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs new file mode 100644 index 0000000000..338e84c472 --- /dev/null +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -0,0 +1,124 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; +using osu.Framework.Utils; +using osu.Game.Overlays.Notifications; +using osuTK; + +namespace osu.Game.Overlays +{ + /// + /// A tray which attaches to the left of to show temporary toasts. + /// + public class NotificationOverlayToastTray : CompositeDrawable + { + public bool IsDisplayingToasts => toastFlow.Count > 0; + + private FillFlowContainer toastFlow = null!; + private BufferedContainer toastContentBackground = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private readonly List pendingToastOperations = new List(); + + private int runningDepth; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Padding = new MarginPadding(20); + + InternalChildren = new Drawable[] + { + toastContentBackground = (new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Colour = ColourInfo.GradientVertical( + colourProvider.Background6.Opacity(0.7f), + colourProvider.Background6.Opacity(0.5f)), + RelativeSizeAxes = Axes.Both, + }.WithEffect(new BlurEffect + { + PadExtent = true, + Sigma = new Vector2(20), + }).With(postEffectDrawable => + { + postEffectDrawable.Scale = new Vector2(1.5f, 1); + postEffectDrawable.Position += new Vector2(70, -50); + postEffectDrawable.AutoSizeAxes = Axes.None; + postEffectDrawable.RelativeSizeAxes = Axes.X; + })), + toastFlow = new FillFlowContainer + { + LayoutDuration = 150, + LayoutEasing = Easing.OutQuart, + Spacing = new Vector2(3), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + }; + } + + public void FlushAllToasts() + { + foreach (var d in pendingToastOperations.Where(d => !d.Completed)) + d.RunTask(); + + pendingToastOperations.Clear(); + } + + public void Post(Notification notification, Action addPermanently) + { + ++runningDepth; + + int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; + + toastFlow.Insert(depth, notification); + + pendingToastOperations.Add(Scheduler.AddDelayed(() => + { + // add notification to permanent overlay unless it was already dismissed by the user. + if (notification.WasClosed) + return; + + toastFlow.Remove(notification); + AddInternal(notification); + + notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint); + notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ => + { + RemoveInternal(notification); + addPermanently(); + + notification.FadeIn(300, Easing.OutQuint); + }); + }, notification.IsImportant ? 15000 : 3000)); + } + + protected override void Update() + { + base.Update(); + + float height = toastFlow.DrawHeight + 120; + float alpha = IsDisplayingToasts ? MathHelper.Clamp(toastFlow.DrawHeight / 40, 0, 1) * toastFlow.Children.Max(n => n.Alpha) : 0; + + toastContentBackground.Height = (float)Interpolation.DampContinuously(toastContentBackground.Height, height, 10, Clock.ElapsedFrameTime); + toastContentBackground.Alpha = (float)Interpolation.DampContinuously(toastContentBackground.Alpha, alpha, 10, Clock.ElapsedFrameTime); + } + } +} From e9cfaa76c954bf2375f3f0c44b24354a2ead697d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 21:04:28 +0900 Subject: [PATCH 440/532] Change global overlay ordering so notification toasts display above settings --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index f7747c5d64..108153fd9d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -804,8 +804,8 @@ namespace osu.Game Children = new Drawable[] { overlayContent = new Container { RelativeSizeAxes = Axes.Both }, - rightFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, leftFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, + rightFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, } }, topMostOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, From 95ce78a50c72df36eaaa2c63101ea9939f930a28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 21:04:38 +0900 Subject: [PATCH 441/532] Reduce notification post delay now that it's less important --- osu.Game/Overlays/NotificationOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 096de558dd..b6354179e0 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -117,7 +117,7 @@ namespace osu.Game.Overlays if (enabled) // we want a slight delay before toggling notifications on to avoid the user becoming overwhelmed. - notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State.Value == Visibility.Visible ? 0 : 1000); + notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State.Value == Visibility.Visible ? 0 : 100); else processingPosts = false; } From a7110666a0c43b214b3f83d48587eadc3067d0ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 21:13:27 +0900 Subject: [PATCH 442/532] Play notification appear sample immediately --- osu.Game/Overlays/NotificationOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index b6354179e0..b0ebf7e666 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -160,6 +160,8 @@ namespace osu.Game.Overlays int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; + playDebouncedSample(notification.PopInSampleName); + if (State.Value == Visibility.Hidden) toastTray.Post(notification, addPermanently); else @@ -172,7 +174,6 @@ namespace osu.Game.Overlays section.Add(notification, depth); updateCounts(); - playDebouncedSample(notification.PopInSampleName); } }); From 403fc18976e8593e93e7724a9069604fd70a242d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 21:13:37 +0900 Subject: [PATCH 443/532] Fix notification completion events not being run when overlay not visible --- osu.Game/Overlays/NotificationOverlay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index b0ebf7e666..80071db37f 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -72,6 +72,7 @@ namespace osu.Game.Overlays }, mainContent = new Container { + AlwaysPresent = true, RelativeSizeAxes = Axes.Both, Children = new Drawable[] { From 224ab29ef405428d9ee4634879fbd6f10658d507 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 21:19:51 +0900 Subject: [PATCH 444/532] Don't dismiss toasts while hovered (and adjust timings slightly) --- osu.Game/Overlays/NotificationOverlayToastTray.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 338e84c472..38f790e88e 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -90,12 +90,20 @@ namespace osu.Game.Overlays toastFlow.Insert(depth, notification); - pendingToastOperations.Add(Scheduler.AddDelayed(() => + pendingToastOperations.Add(scheduleDismissal()); + + ScheduledDelegate scheduleDismissal() => Scheduler.AddDelayed(() => { // add notification to permanent overlay unless it was already dismissed by the user. if (notification.WasClosed) return; + if (notification.IsHovered) + { + pendingToastOperations.Add(scheduleDismissal()); + return; + } + toastFlow.Remove(notification); AddInternal(notification); @@ -107,7 +115,7 @@ namespace osu.Game.Overlays notification.FadeIn(300, Easing.OutQuint); }); - }, notification.IsImportant ? 15000 : 3000)); + }, notification.IsImportant ? 12000 : 2500); } protected override void Update() From b185194d07f0ff1e6ebe7eafb796a85491fab118 Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Tue, 30 Aug 2022 14:44:44 +0200 Subject: [PATCH 445/532] Apply comments by smoogi --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 1 - .../TestSceneDrumRollJudgements.cs | 38 ------------------- .../Judgements/TaikoDrumRollJudgement.cs | 16 -------- .../Objects/Drawables/DrawableDrumRoll.cs | 2 +- .../Objects/Drawables/DrawableSwell.cs | 3 +- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 2 +- 6 files changed, 3 insertions(+), 59 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollJudgements.cs delete mode 100644 osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index ef95358d34..637e355d97 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -118,7 +118,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail); - assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Idle); } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollJudgements.cs deleted file mode 100644 index 82dfaecaa4..0000000000 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollJudgements.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using System.Linq; -using NUnit.Framework; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Taiko.Objects; - -namespace osu.Game.Rulesets.Taiko.Tests -{ - public class TestSceneDrumRollJudgements : TestSceneTaikoPlayer - { - [Test] - public void TestStrongDrumRollFullyJudgedOnKilled() - { - AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted.Value); - AddAssert("all judgements are misses", () => Player.Results.All(r => r.Type == r.Judgement.MinResult)); - } - - protected override bool Autoplay => false; - - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap - { - BeatmapInfo = { Ruleset = new TaikoRuleset().RulesetInfo }, - HitObjects = - { - new DrumRoll - { - StartTime = 1000, - Duration = 1000, - IsStrong = true - } - } - }; - } -} diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs deleted file mode 100644 index f7f923e76e..0000000000 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Taiko.Judgements -{ - public class TaikoDrumRollJudgement : TaikoJudgement - { - public override HitResult MaxResult => HitResult.IgnoreHit; - - protected override double HealthIncreaseFor(HitResult result) => 0; - } -} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 418c4673e2..259add81e0 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (timeOffset < 0) return; - ApplyResult(r => r.Type = r.Judgement.MinResult); + ApplyResult(r => r.Type = r.Judgement.MaxResult); } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 84edb30890..a6f6edba09 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -16,7 +16,6 @@ using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; @@ -224,7 +223,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables tick.TriggerResult(false); } - ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.SmallBonus : r.Judgement.MinResult); + ApplyResult(r => r.Type = numHits == HitObject.RequiredHits ? r.Judgement.MaxResult : r.Judgement.MinResult); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index db752c6691..bd6f10b1a4 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Objects } } - public override Judgement CreateJudgement() => new TaikoDrumRollJudgement(); + public override Judgement CreateJudgement() => new IgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; From a9aa928ce6b50162b17ca8bf0b889e4e4e0a0586 Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Tue, 30 Aug 2022 15:00:46 +0200 Subject: [PATCH 446/532] Fix test, make strong hits have LargeBonus judgement --- osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs index f2397aba22..bafe7dfbaf 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoStrongJudgement : TaikoJudgement { - public override HitResult MaxResult => HitResult.SmallBonus; + public override HitResult MaxResult => HitResult.LargeBonus; // MainObject already changes the HP protected override double HealthIncreaseFor(HitResult result) => 0; diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index bd6f10b1a4..3dab8d2e13 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -11,7 +11,6 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Judgements; using osuTK; namespace osu.Game.Rulesets.Taiko.Objects From ed11b1ba6f57063527ec2fb96965c64671365b8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Aug 2022 21:31:06 +0900 Subject: [PATCH 447/532] Improve forwarding flow to not use piling delegates --- osu.Game/Overlays/NotificationOverlay.cs | 30 ++++----- .../Overlays/NotificationOverlayToastTray.cs | 62 +++++++++++-------- 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 80071db37f..593618ab51 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -68,6 +68,7 @@ namespace osu.Game.Overlays { toastTray = new NotificationOverlayToastTray { + ForwardNotificationToPermanentStore = addPermanently, Origin = Anchor.TopRight, }, mainContent = new Container @@ -157,27 +158,26 @@ namespace osu.Game.Overlays if (notification is IHasCompletionTarget hasCompletionTarget) hasCompletionTarget.CompletionTarget = Post; - var ourType = notification.GetType(); - - int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; - playDebouncedSample(notification.PopInSampleName); if (State.Value == Visibility.Hidden) - toastTray.Post(notification, addPermanently); + toastTray.Post(notification); else - addPermanently(); - - void addPermanently() - { - var section = sections.Children.First(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); - - section.Add(notification, depth); - - updateCounts(); - } + addPermanently(notification); }); + private void addPermanently(Notification notification) + { + var ourType = notification.GetType(); + int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; + + var section = sections.Children.First(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType))); + + section.Add(notification, depth); + + updateCounts(); + } + protected override void Update() { base.Update(); diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 38f790e88e..7017365dbd 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; -using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Overlays.Notifications; using osuTK; @@ -25,13 +24,13 @@ namespace osu.Game.Overlays { public bool IsDisplayingToasts => toastFlow.Count > 0; - private FillFlowContainer toastFlow = null!; + private FillFlowContainer toastFlow = null!; private BufferedContainer toastContentBackground = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - private readonly List pendingToastOperations = new List(); + public Action? ForwardNotificationToPermanentStore { get; set; } private int runningDepth; @@ -63,7 +62,7 @@ namespace osu.Game.Overlays postEffectDrawable.AutoSizeAxes = Axes.None; postEffectDrawable.RelativeSizeAxes = Axes.X; })), - toastFlow = new FillFlowContainer + toastFlow = new FillFlowContainer { LayoutDuration = 150, LayoutEasing = Easing.OutQuart, @@ -76,13 +75,13 @@ namespace osu.Game.Overlays public void FlushAllToasts() { - foreach (var d in pendingToastOperations.Where(d => !d.Completed)) - d.RunTask(); - - pendingToastOperations.Clear(); + foreach (var notification in toastFlow.ToArray()) + { + forwardNotification(notification); + } } - public void Post(Notification notification, Action addPermanently) + public void Post(Notification notification) { ++runningDepth; @@ -90,34 +89,47 @@ namespace osu.Game.Overlays toastFlow.Insert(depth, notification); - pendingToastOperations.Add(scheduleDismissal()); + scheduleDismissal(); - ScheduledDelegate scheduleDismissal() => Scheduler.AddDelayed(() => + void scheduleDismissal() => Scheduler.AddDelayed(() => { - // add notification to permanent overlay unless it was already dismissed by the user. + // Notification dismissed by user. if (notification.WasClosed) return; + // Notification forwarded away. + if (notification.Parent != toastFlow) + return; + + // Notification hovered; delay dismissal. if (notification.IsHovered) { - pendingToastOperations.Add(scheduleDismissal()); + scheduleDismissal(); return; } - toastFlow.Remove(notification); - AddInternal(notification); - - notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint); - notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ => - { - RemoveInternal(notification); - addPermanently(); - - notification.FadeIn(300, Easing.OutQuint); - }); + // All looks good, forward away! + forwardNotification(notification); }, notification.IsImportant ? 12000 : 2500); } + private void forwardNotification(Notification notification) + { + Debug.Assert(notification.Parent == toastFlow); + + toastFlow.Remove(notification); + AddInternal(notification); + + notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint); + notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ => + { + RemoveInternal(notification); + ForwardNotificationToPermanentStore?.Invoke(notification); + + notification.FadeIn(300, Easing.OutQuint); + }); + } + protected override void Update() { base.Update(); From a62ba9e0d9bda715dabc408df5b0b1f85ed76cd2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 00:57:18 +0900 Subject: [PATCH 448/532] Remove notification blocking behaviour of first run setup --- .../Visual/Navigation/TestSceneFirstRunGame.cs | 9 --------- osu.Game/Overlays/NotificationOverlay.cs | 10 ++-------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneFirstRunGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneFirstRunGame.cs index f4078676c9..fe26d59812 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneFirstRunGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneFirstRunGame.cs @@ -26,16 +26,7 @@ namespace osu.Game.Tests.Visual.Navigation public void TestImportantNotificationDoesntInterruptSetup() { AddStep("post important notification", () => Game.Notifications.Post(new SimpleNotification { Text = "Important notification" })); - AddAssert("no notification posted", () => Game.Notifications.UnreadCount.Value == 0); AddAssert("first-run setup still visible", () => Game.FirstRunOverlay.State.Value == Visibility.Visible); - - AddUntilStep("finish first-run setup", () => - { - Game.FirstRunOverlay.NextButton.TriggerClick(); - return Game.FirstRunOverlay.State.Value == Visibility.Hidden; - }); - AddWaitStep("wait for post delay", 5); - AddAssert("notifications shown", () => Game.Notifications.State.Value == Visibility.Visible); AddAssert("notification posted", () => Game.Notifications.UnreadCount.Value == 1); } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 593618ab51..bb0beeef41 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -38,8 +38,6 @@ namespace osu.Game.Overlays [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private readonly IBindable firstRunSetupVisibility = new Bindable(); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { if (State.Value == Visibility.Visible) @@ -58,7 +56,7 @@ namespace osu.Game.Overlays private Container mainContent = null!; [BackgroundDependencyLoader] - private void load(FirstRunSetupOverlay? firstRunSetup) + private void load() { X = WIDTH; Width = WIDTH; @@ -104,16 +102,13 @@ namespace osu.Game.Overlays } }, }; - - if (firstRunSetup != null) - firstRunSetupVisibility.BindTo(firstRunSetup.State); } private ScheduledDelegate? notificationsEnabler; private void updateProcessingMode() { - bool enabled = (OverlayActivationMode.Value == OverlayActivation.All && firstRunSetupVisibility.Value != Visibility.Visible) || State.Value == Visibility.Visible; + bool enabled = OverlayActivationMode.Value == OverlayActivation.All || State.Value == Visibility.Visible; notificationsEnabler?.Cancel(); @@ -129,7 +124,6 @@ namespace osu.Game.Overlays base.LoadComplete(); State.BindValueChanged(_ => updateProcessingMode()); - firstRunSetupVisibility.BindValueChanged(_ => updateProcessingMode()); OverlayActivationMode.BindValueChanged(_ => updateProcessingMode(), true); } From 31a9980686a620c5e434a0ea55851e112fe183a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 00:57:30 +0900 Subject: [PATCH 449/532] Update remaining test expectations with new behaviour --- .../Visual/UserInterface/TestSceneNotificationOverlay.cs | 5 +++-- osu.Game/Overlays/NotificationOverlay.cs | 7 ++++++- osu.Game/Overlays/NotificationOverlayToastTray.cs | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index c196b204b9..38eecaa052 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -110,7 +110,8 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep(@"simple #1", sendHelloNotification); - AddAssert("Is visible", () => notificationOverlay.State.Value == Visibility.Visible); + AddAssert("toast displayed", () => notificationOverlay.ToastCount == 1); + AddAssert("is not visible", () => notificationOverlay.State.Value == Visibility.Hidden); checkDisplayedCount(1); @@ -183,7 +184,7 @@ namespace osu.Game.Tests.Visual.UserInterface } private void checkDisplayedCount(int expected) => - AddAssert($"Displayed count is {expected}", () => notificationOverlay.UnreadCount.Value == expected); + AddUntilStep($"Displayed count is {expected}", () => notificationOverlay.UnreadCount.Value == expected); private void sendDownloadProgress() { diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index bb0beeef41..3769a7080f 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -129,6 +129,8 @@ namespace osu.Game.Overlays public IBindable UnreadCount => unreadCount; + public int ToastCount => toastTray.UnreadCount; + private readonly BindableInt unreadCount = new BindableInt(); private int runningDepth; @@ -155,7 +157,10 @@ namespace osu.Game.Overlays playDebouncedSample(notification.PopInSampleName); if (State.Value == Visibility.Hidden) + { toastTray.Post(notification); + updateCounts(); + } else addPermanently(notification); }); @@ -220,7 +225,7 @@ namespace osu.Game.Overlays private void updateCounts() { - unreadCount.Value = sections.Select(c => c.UnreadCount).Sum(); + unreadCount.Value = sections.Select(c => c.UnreadCount).Sum() + toastTray.UnreadCount; } private void markAllRead() diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 7017365dbd..368996f395 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -32,6 +32,8 @@ namespace osu.Game.Overlays public Action? ForwardNotificationToPermanentStore { get; set; } + public int UnreadCount => toastFlow.Count(n => !n.WasClosed && !n.Read); + private int runningDepth; [BackgroundDependencyLoader] From 9eb615f9421202ebec908f97c5a1524005e0f3dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 01:40:27 +0900 Subject: [PATCH 450/532] Fix remaining test failures by strengthening `PlayerLoader` tests - Click using `TriggerClick` as notifications move around quite a bit. - Ensure any notifications from a previous test method are cleaned up. --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 44 ++++++++++--------- osu.Game/Overlays/NotificationOverlay.cs | 6 +-- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 922529cf19..b6c17fbaca 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -14,11 +14,10 @@ using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; -using osu.Framework.Utils; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Configuration; -using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Mods; @@ -30,7 +29,6 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Utils; using osuTK.Input; -using SkipOverlay = osu.Game.Screens.Play.SkipOverlay; namespace osu.Game.Tests.Visual.Gameplay { @@ -83,6 +81,20 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUp] public void Setup() => Schedule(() => player = null); + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("read all notifications", () => + { + notificationOverlay.Show(); + notificationOverlay.Hide(); + }); + + AddUntilStep("wait for no notifications", () => notificationOverlay.UnreadCount.Value, () => Is.EqualTo(0)); + } + /// /// Sets the input manager child to a new test player loader container instance. /// @@ -287,16 +299,9 @@ namespace osu.Game.Tests.Visual.Gameplay saveVolumes(); - AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == 1); - AddStep("click notification", () => - { - var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last(); - var flowContainer = scrollContainer.Children.OfType>().First(); - var notification = flowContainer.First(); + AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value, () => Is.EqualTo(1)); - InputManager.MoveMouseTo(notification); - InputManager.Click(MouseButton.Left); - }); + clickNotificationIfAny(); AddAssert("check " + volumeName, assert); @@ -366,15 +371,7 @@ namespace osu.Game.Tests.Visual.Gameplay })); AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); AddAssert($"notification {(shouldWarn ? "triggered" : "not triggered")}", () => notificationOverlay.UnreadCount.Value == (shouldWarn ? 1 : 0)); - AddStep("click notification", () => - { - var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last(); - var flowContainer = scrollContainer.Children.OfType>().First(); - var notification = flowContainer.First(); - - InputManager.MoveMouseTo(notification); - InputManager.Click(MouseButton.Left); - }); + clickNotificationIfAny(); AddUntilStep("wait for player load", () => player.IsLoaded); } @@ -439,6 +436,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("skip button not visible", () => !checkSkipButtonVisible()); } + private void clickNotificationIfAny() + { + AddStep("click notification", () => notificationOverlay.ChildrenOfType().FirstOrDefault()?.TriggerClick()); + } + private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(); private class TestPlayerLoader : PlayerLoader diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 3769a7080f..488f3eab0d 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -157,12 +157,11 @@ namespace osu.Game.Overlays playDebouncedSample(notification.PopInSampleName); if (State.Value == Visibility.Hidden) - { toastTray.Post(notification); - updateCounts(); - } else addPermanently(notification); + + updateCounts(); }); private void addPermanently(Notification notification) @@ -231,7 +230,6 @@ namespace osu.Game.Overlays private void markAllRead() { sections.Children.ForEach(s => s.MarkAllRead()); - updateCounts(); } } From ad650adab0b9ea2b226b10523193f96439265c6c Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Tue, 30 Aug 2022 18:03:44 +0100 Subject: [PATCH 451/532] Fix speed note count sum --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index a156726f94..c39d61020c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (maxStrain == 0) return 0; - return objectStrains.Aggregate((total, next) => total + (1.0 / (1.0 + Math.Exp(-(next / maxStrain * 12.0 - 6.0))))); + return objectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0)))); } } } From 0558dae917899a7cafb5dd14adaf016ff7701ce0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 12:46:03 +0900 Subject: [PATCH 452/532] Mark toasts as read when closing the overlay for added safety I'm not sure how the read status will play out going forward so I'm just adding this to keep things conforming for now. --- osu.Game/Overlays/NotificationOverlay.cs | 1 + osu.Game/Overlays/NotificationOverlayToastTray.cs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 488f3eab0d..6eb327cbce 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -230,6 +230,7 @@ namespace osu.Game.Overlays private void markAllRead() { sections.Children.ForEach(s => s.MarkAllRead()); + toastTray.MarkAllRead(); updateCounts(); } } diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 368996f395..800751072c 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -75,6 +75,12 @@ namespace osu.Game.Overlays }; } + public void MarkAllRead() + { + toastFlow.Children.ForEach(n => n.Read = true); + InternalChildren.OfType().ForEach(n => n.Read = true); + } + public void FlushAllToasts() { foreach (var notification in toastFlow.ToArray()) From 7c72c6b43faef9a78400a8b0f5807fc8f927885f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 12:46:43 +0900 Subject: [PATCH 453/532] Fix unread count potentially missing notifications in a transforming state --- osu.Game/Overlays/NotificationOverlay.cs | 10 +++++----- osu.Game/Overlays/NotificationOverlayToastTray.cs | 9 +++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 6eb327cbce..2c39ebcc87 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -222,16 +222,16 @@ namespace osu.Game.Overlays } } - private void updateCounts() - { - unreadCount.Value = sections.Select(c => c.UnreadCount).Sum() + toastTray.UnreadCount; - } - private void markAllRead() { sections.Children.ForEach(s => s.MarkAllRead()); toastTray.MarkAllRead(); updateCounts(); } + + private void updateCounts() + { + unreadCount.Value = sections.Select(c => c.UnreadCount).Sum() + toastTray.UnreadCount; + } } } diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 800751072c..4417b5e0d0 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -32,7 +33,8 @@ namespace osu.Game.Overlays public Action? ForwardNotificationToPermanentStore { get; set; } - public int UnreadCount => toastFlow.Count(n => !n.WasClosed && !n.Read); + public int UnreadCount => toastFlow.Count(n => !n.WasClosed && !n.Read) + + InternalChildren.OfType().Count(n => !n.WasClosed && !n.Read); private int runningDepth; @@ -64,7 +66,7 @@ namespace osu.Game.Overlays postEffectDrawable.AutoSizeAxes = Axes.None; postEffectDrawable.RelativeSizeAxes = Axes.X; })), - toastFlow = new FillFlowContainer + toastFlow = new AlwaysUpdateFillFlowContainer { LayoutDuration = 150, LayoutEasing = Easing.OutQuart, @@ -84,9 +86,7 @@ namespace osu.Game.Overlays public void FlushAllToasts() { foreach (var notification in toastFlow.ToArray()) - { forwardNotification(notification); - } } public void Post(Notification notification) @@ -125,6 +125,7 @@ namespace osu.Game.Overlays { Debug.Assert(notification.Parent == toastFlow); + // Temporarily remove from flow so we can animate the position off to the right. toastFlow.Remove(notification); AddInternal(notification); From c573396ab6f5c1ff4d66d89cc3e73eaaa0aefdc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 12:46:54 +0900 Subject: [PATCH 454/532] Fix `IntroTestScene` not clearing previous notifications hard enough --- osu.Game.Tests/Visual/Menus/IntroTestScene.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs index 66e3612113..e0f0df0554 100644 --- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs +++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs @@ -52,6 +52,7 @@ namespace osu.Game.Tests.Visual.Menus }, notifications = new NotificationOverlay { + Depth = float.MinValue, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, } @@ -82,7 +83,14 @@ namespace osu.Game.Tests.Visual.Menus [Test] public virtual void TestPlayIntroWithFailingAudioDevice() { - AddStep("hide notifications", () => notifications.Hide()); + AddStep("reset notifications", () => + { + notifications.Show(); + notifications.Hide(); + }); + + AddUntilStep("wait for no notifications", () => notifications.UnreadCount.Value, () => Is.EqualTo(0)); + AddStep("restart sequence", () => { logo.FinishTransforms(); From 85442fe0328395bb76209b7478bbf69f0a85a5cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 12:48:30 +0900 Subject: [PATCH 455/532] Adjust dismiss button background colour to avoid conflict with background --- osu.Game/Overlays/Notifications/Notification.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 90d9b88b79..a2ffdbe208 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; using osuTK.Graphics; @@ -217,7 +218,7 @@ namespace osu.Game.Overlays.Notifications { background = new Box { - Colour = colourProvider.Background4, + Colour = OsuColour.Gray(0).Opacity(0.15f), Alpha = 0, RelativeSizeAxes = Axes.Both, }, From 8b9ccc66b7b9bd8c01385083b9b55fd9943f5e68 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 12:49:28 +0900 Subject: [PATCH 456/532] Update `ProgressNotification` font spec to match other notifications --- osu.Game/Overlays/Notifications/ProgressNotification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 36b8bac873..14cf6b3013 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -173,7 +173,7 @@ namespace osu.Game.Overlays.Notifications public ProgressNotification() { - Content.Add(textDrawable = new OsuTextFlowContainer + Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium)) { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, From 7ce1cf7560d00b9c92df6a6b18eda8a5999470d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 13:19:58 +0900 Subject: [PATCH 457/532] Add test coverage of skip button failure with equal time --- .../Visual/Gameplay/TestSceneSkipOverlay.cs | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index b6b3650c83..6b02449aa3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -27,8 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay private const double skip_time = 6000; - [SetUp] - public void SetUp() => Schedule(() => + private void createTest(double skipTime = skip_time) => AddStep("create test", () => { requestCount = 0; increment = skip_time; @@ -40,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - skip = new TestSkipOverlay(skip_time) + skip = new TestSkipOverlay(skipTime) { RequestSkip = () => { @@ -55,9 +54,25 @@ namespace osu.Game.Tests.Visual.Gameplay gameplayClock = gameplayClockContainer; }); + [Test] + public void TestSkipTimeZero() + { + createTest(0); + AddUntilStep("wait for skip overlay expired", () => !skip.IsAlive); + } + + [Test] + public void TestSkipTimeEqualToSkip() + { + createTest(MasterGameplayClockContainer.MINIMUM_SKIP_TIME); + AddUntilStep("wait for skip overlay expired", () => !skip.IsAlive); + } + [Test] public void TestFadeOnIdle() { + createTest(); + AddStep("move mouse", () => InputManager.MoveMouseTo(Vector2.Zero)); AddUntilStep("fully visible", () => skip.FadingContent.Alpha == 1); AddUntilStep("wait for fade", () => skip.FadingContent.Alpha < 1); @@ -70,6 +85,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestClickableAfterFade() { + createTest(); + AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); AddUntilStep("wait for fade", () => skip.FadingContent.Alpha == 0); AddStep("click", () => InputManager.Click(MouseButton.Left)); @@ -79,6 +96,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestClickOnlyActuatesOnce() { + createTest(); + AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); AddStep("click", () => { @@ -94,6 +113,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestClickOnlyActuatesMultipleTimes() { + createTest(); + AddStep("set increment lower", () => increment = 3000); AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); AddStep("click", () => InputManager.Click(MouseButton.Left)); @@ -106,6 +127,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestDoesntFadeOnMouseDown() { + createTest(); + AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); AddStep("button down", () => InputManager.PressButton(MouseButton.Left)); AddUntilStep("wait for overlay disappear", () => !skip.OverlayContent.IsPresent); From 51346e015417f634928c169d641a332f9bae0590 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 13:05:57 +0900 Subject: [PATCH 458/532] Fix skip button getting stuck on screen for certain beatmaps Closes #20034. --- osu.Game/Screens/Play/SkipOverlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 5c9a706549..974d40b538 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -114,16 +114,17 @@ namespace osu.Game.Screens.Play { base.LoadComplete(); + displayTime = gameplayClock.CurrentTime; + // skip is not required if there is no extra "empty" time to skip. // we may need to remove this if rewinding before the initial player load position becomes a thing. - if (fadeOutBeginTime < gameplayClock.CurrentTime) + if (fadeOutBeginTime <= displayTime) { Expire(); return; } button.Action = () => RequestSkip?.Invoke(); - displayTime = gameplayClock.CurrentTime; fadeContainer.TriggerShow(); @@ -146,7 +147,12 @@ namespace osu.Game.Screens.Play { base.Update(); - double progress = fadeOutBeginTime <= displayTime ? 1 : Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime)); + // This case causes an immediate expire in `LoadComplete`, but `Update` may run once after that. + // Avoid div-by-zero below. + if (fadeOutBeginTime <= displayTime) + return; + + double progress = Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime)); remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); From 93bc4b9294cf08d32d6ba6af00ccf72200a79cc3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 13:44:06 +0900 Subject: [PATCH 459/532] Add toggle for tournament client "auto progression" behaviour Addresses https://github.com/ppy/osu/discussions/20038. --- osu.Game.Tournament/Models/LadderInfo.cs | 2 ++ .../Screens/Gameplay/GameplayScreen.cs | 21 +++++++++++-------- .../Screens/MapPool/MapPoolScreen.cs | 9 +++++--- .../Screens/Setup/SetupScreen.cs | 6 ++++++ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index 0f73b60774..6b64a1156e 100644 --- a/osu.Game.Tournament/Models/LadderInfo.cs +++ b/osu.Game.Tournament/Models/LadderInfo.cs @@ -40,5 +40,7 @@ namespace osu.Game.Tournament.Models MinValue = 3, MaxValue = 4, }; + + public Bindable AutoProgressScreens = new BindableBool(true); } } diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 54ae4c0366..8a23ee65da 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -199,16 +199,19 @@ namespace osu.Game.Tournament.Screens.Gameplay case TourneyState.Idle: contract(); - const float delay_before_progression = 4000; - - // if we've returned to idle and the last screen was ranking - // we should automatically proceed after a short delay - if (lastState == TourneyState.Ranking && !warmup.Value) + if (LadderInfo.AutoProgressScreens.Value) { - if (CurrentMatch.Value?.Completed.Value == true) - scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(TeamWinScreen)); }, delay_before_progression); - else if (CurrentMatch.Value?.Completed.Value == false) - scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(MapPoolScreen)); }, delay_before_progression); + const float delay_before_progression = 4000; + + // if we've returned to idle and the last screen was ranking + // we should automatically proceed after a short delay + if (lastState == TourneyState.Ranking && !warmup.Value) + { + if (CurrentMatch.Value?.Completed.Value == true) + scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(TeamWinScreen)); }, delay_before_progression); + else if (CurrentMatch.Value?.Completed.Value == false) + scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(MapPoolScreen)); }, delay_before_progression); + } } break; diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 5eb2142fae..4d36515316 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -197,10 +197,13 @@ namespace osu.Game.Tournament.Screens.MapPool setNextMode(); - if (pickType == ChoiceType.Pick && CurrentMatch.Value.PicksBans.Any(i => i.Type == ChoiceType.Pick)) + if (LadderInfo.AutoProgressScreens.Value) { - scheduledChange?.Cancel(); - scheduledChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(GameplayScreen)); }, 10000); + if (pickType == ChoiceType.Pick && CurrentMatch.Value.PicksBans.Any(i => i.Type == ChoiceType.Pick)) + { + scheduledChange?.Cancel(); + scheduledChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(GameplayScreen)); }, 10000); + } } } diff --git a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs index 2b2dce3664..ff781dec80 100644 --- a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs @@ -131,6 +131,12 @@ namespace osu.Game.Tournament.Screens.Setup windowSize.Value = new Size((int)(height * aspect_ratio / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), height); } }, + new LabelledSwitchButton + { + Label = "Auto advance screens", + Description = "Screens will progress automatically from gameplay -> results -> map pool", + Current = LadderInfo.AutoProgressScreens, + }, }; } From e9463f3c192aa635dc73c891e61ceb58206572e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 14:07:55 +0900 Subject: [PATCH 460/532] Test editor `ComposeScreen` tests not adding beatmap to hierarchy Makes it hard to test anything because `EditorBeatmap`'s `Update` method updates whether a beatmap has timing or not (enabling the placement controls). Also adds a basic timing point to allow for better testing. --- .../Editor/TestSceneManiaComposeScreen.cs | 15 ++++++++++++--- .../Visual/Editing/TestSceneComposeScreen.cs | 11 +++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs index 15b38b39ba..0354228cca 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Database; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; @@ -34,10 +35,14 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { AddStep("setup compose screen", () => { - var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }) + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 4 }) { BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, - }); + }; + + beatmap.ControlPointInfo.Add(0, new TimingControlPoint()); + + var editorBeatmap = new EditorBeatmap(beatmap, new LegacyBeatmapSkin(beatmap.BeatmapInfo, null)); Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); @@ -50,7 +55,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor (typeof(IBeatSnapProvider), editorBeatmap), (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)), }, - Child = new ComposeScreen { State = { Value = Visibility.Visible } }, + Children = new Drawable[] + { + editorBeatmap, + new ComposeScreen { State = { Value = Visibility.Visible } }, + } }; }); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs index 291630fa3a..0df44b9ac4 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu; @@ -34,9 +35,11 @@ namespace osu.Game.Tests.Visual.Editing { var beatmap = new OsuBeatmap { - BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } + BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, }; + beatmap.ControlPointInfo.Add(0, new TimingControlPoint()); + editorBeatmap = new EditorBeatmap(beatmap, new LegacyBeatmapSkin(beatmap.BeatmapInfo, null)); Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); @@ -50,7 +53,11 @@ namespace osu.Game.Tests.Visual.Editing (typeof(IBeatSnapProvider), editorBeatmap), (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)), }, - Child = new ComposeScreen { State = { Value = Visibility.Visible } }, + Children = new Drawable[] + { + editorBeatmap, + new ComposeScreen { State = { Value = Visibility.Visible } }, + } }; }); From cc9dc604a069506f443aa8d3f77bcc35d9d4025a Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 31 Aug 2022 17:21:58 +0900 Subject: [PATCH 461/532] Refactor feedback sample playback logic --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 124 +++++++++--------- 1 file changed, 59 insertions(+), 65 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 1984840553..a5f133506e 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -40,30 +42,26 @@ namespace osu.Game.Graphics.UserInterface Margin = new MarginPadding { Left = 2 }, }; - private readonly Sample?[] textAddedSamples = new Sample[4]; - private Sample? capsTextAddedSample; - private Sample? textRemovedSample; - private Sample? textCommittedSample; - private Sample? caretMovedSample; - - private Sample? selectCharSample; - private Sample? selectWordSample; - private Sample? selectAllSample; - private Sample? deselectSample; - private OsuCaret? caret; private bool selectionStarted; private double sampleLastPlaybackTime; - private enum SelectionSampleType + private enum FeedbackSampleType { - Character, - Word, - All, + TextAdd, + TextAddCaps, + TextRemove, + TextConfirm, + CaretMove, + SelectCharacter, + SelectWord, + SelectAll, Deselect } + private Dictionary sampleMap = new Dictionary(); + public OsuTextBox() { Height = 40; @@ -87,18 +85,22 @@ namespace osu.Game.Graphics.UserInterface Placeholder.Colour = colourProvider?.Foreground1 ?? new Color4(180, 180, 180, 255); + var textAddedSamples = new Sample?[4]; for (int i = 0; i < textAddedSamples.Length; i++) textAddedSamples[i] = audio.Samples.Get($@"Keyboard/key-press-{1 + i}"); - capsTextAddedSample = audio.Samples.Get(@"Keyboard/key-caps"); - textRemovedSample = audio.Samples.Get(@"Keyboard/key-delete"); - textCommittedSample = audio.Samples.Get(@"Keyboard/key-confirm"); - caretMovedSample = audio.Samples.Get(@"Keyboard/key-movement"); - - selectCharSample = audio.Samples.Get(@"Keyboard/select-char"); - selectWordSample = audio.Samples.Get(@"Keyboard/select-word"); - selectAllSample = audio.Samples.Get(@"Keyboard/select-all"); - deselectSample = audio.Samples.Get(@"Keyboard/deselect"); + sampleMap = new Dictionary + { + { FeedbackSampleType.TextAdd, textAddedSamples }, + { FeedbackSampleType.TextAddCaps, new[] { audio.Samples.Get(@"Keyboard/key-caps") } }, + { FeedbackSampleType.TextRemove, new[] { audio.Samples.Get(@"Keyboard/key-delete") } }, + { FeedbackSampleType.TextConfirm, new[] { audio.Samples.Get(@"Keyboard/key-confirm") } }, + { FeedbackSampleType.CaretMove, new[] { audio.Samples.Get(@"Keyboard/key-movement") } }, + { FeedbackSampleType.SelectCharacter, new[] { audio.Samples.Get(@"Keyboard/select-char") } }, + { FeedbackSampleType.SelectWord, new[] { audio.Samples.Get(@"Keyboard/select-word") } }, + { FeedbackSampleType.SelectAll, new[] { audio.Samples.Get(@"Keyboard/select-all") } }, + { FeedbackSampleType.Deselect, new[] { audio.Samples.Get(@"Keyboard/deselect") } } + }; } private Color4 selectionColour; @@ -110,23 +112,23 @@ namespace osu.Game.Graphics.UserInterface base.OnUserTextAdded(added); if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples) - capsTextAddedSample?.Play(); + playSample(FeedbackSampleType.TextAddCaps); else - playTextAddedSample(); + playSample(FeedbackSampleType.TextAdd); } protected override void OnUserTextRemoved(string removed) { base.OnUserTextRemoved(removed); - textRemovedSample?.Play(); + playSample(FeedbackSampleType.TextRemove); } protected override void OnTextCommitted(bool textChanged) { base.OnTextCommitted(textChanged); - textCommittedSample?.Play(); + playSample(FeedbackSampleType.TextConfirm); } protected override void OnCaretMoved(bool selecting) @@ -134,7 +136,7 @@ namespace osu.Game.Graphics.UserInterface base.OnCaretMoved(selecting); if (!selecting) - caretMovedSample?.Play(); + playSample(FeedbackSampleType.CaretMove); } protected override void OnTextSelectionChanged(TextSelectionType selectionType) @@ -144,15 +146,15 @@ namespace osu.Game.Graphics.UserInterface switch (selectionType) { case TextSelectionType.Character: - playSelectSample(SelectionSampleType.Character); + playSample(FeedbackSampleType.SelectCharacter); break; case TextSelectionType.Word: - playSelectSample(selectionStarted ? SelectionSampleType.Character : SelectionSampleType.Word); + playSample(selectionStarted ? FeedbackSampleType.SelectCharacter : FeedbackSampleType.SelectWord); break; case TextSelectionType.All: - playSelectSample(SelectionSampleType.All); + playSample(FeedbackSampleType.SelectAll); break; } @@ -165,7 +167,7 @@ namespace osu.Game.Graphics.UserInterface if (!selectionStarted) return; - playSelectSample(SelectionSampleType.Deselect); + playSample(FeedbackSampleType.Deselect); selectionStarted = false; } @@ -184,13 +186,13 @@ namespace osu.Game.Graphics.UserInterface case 1: // composition probably ended by pressing backspace, or was cancelled. - textRemovedSample?.Play(); + playSample(FeedbackSampleType.TextRemove); return; default: // longer text removed, composition ended because it was cancelled. // could be a different sample if desired. - textRemovedSample?.Play(); + playSample(FeedbackSampleType.TextRemove); return; } } @@ -198,7 +200,7 @@ namespace osu.Game.Graphics.UserInterface if (addedTextLength > 0) { // some text was added, probably due to typing new text or by changing the candidate. - playTextAddedSample(); + playSample(FeedbackSampleType.TextAdd); return; } @@ -206,14 +208,14 @@ namespace osu.Game.Graphics.UserInterface { // text was probably removed by backspacing. // it's also possible that a candidate that only removed text was changed to. - textRemovedSample?.Play(); + playSample(FeedbackSampleType.TextRemove); return; } if (caretMoved) { // only the caret/selection was moved. - caretMovedSample?.Play(); + playSample(FeedbackSampleType.CaretMove); } } @@ -224,13 +226,13 @@ namespace osu.Game.Graphics.UserInterface if (successful) { // composition was successfully completed, usually by pressing the enter key. - textCommittedSample?.Play(); + playSample(FeedbackSampleType.TextConfirm); } else { // composition was prematurely ended, eg. by clicking inside the textbox. // could be a different sample if desired. - textCommittedSample?.Play(); + playSample(FeedbackSampleType.TextConfirm); } } @@ -259,43 +261,35 @@ namespace osu.Game.Graphics.UserInterface SelectionColour = SelectionColour, }; - private void playSelectSample(SelectionSampleType selectionType) + private SampleChannel? getSampleChannel(FeedbackSampleType feedbackSampleType) + { + var samples = sampleMap[feedbackSampleType]; + + if (samples == null || samples.Length == 0) + return null; + + return samples[RNG.Next(0, samples.Length)]?.GetChannel(); + } + + private void playSample(FeedbackSampleType feedbackSample) { if (Time.Current < sampleLastPlaybackTime + 15) return; - SampleChannel? channel; - double pitch = 0.98 + RNG.NextDouble(0.04); - - switch (selectionType) - { - case SelectionSampleType.All: - channel = selectAllSample?.GetChannel(); - break; - - case SelectionSampleType.Word: - channel = selectWordSample?.GetChannel(); - break; - - case SelectionSampleType.Deselect: - channel = deselectSample?.GetChannel(); - break; - - default: - channel = selectCharSample?.GetChannel(); - pitch += (SelectedText.Length / (double)Text.Length) * 0.15f; - break; - } + SampleChannel? channel = getSampleChannel(feedbackSample); if (channel == null) return; + double pitch = 0.98 + RNG.NextDouble(0.04); + + if (feedbackSample == FeedbackSampleType.SelectCharacter) + pitch += ((double)SelectedText.Length / Math.Max(1, Text.Length)) * 0.15f; + channel.Frequency.Value = pitch; channel.Play(); sampleLastPlaybackTime = Time.Current; } - private void playTextAddedSample() => textAddedSamples[RNG.Next(0, textAddedSamples.Length)]?.Play(); - private class OsuCaret : Caret { private const float caret_move_time = 60; From 212d76a11f3b3bbb6e8bafdcc9c935076a34c61c Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 31 Aug 2022 17:23:22 +0900 Subject: [PATCH 462/532] Add audio feedback for invalid textbox input --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index a5f133506e..e5341cfd4b 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -53,6 +53,7 @@ namespace osu.Game.Graphics.UserInterface TextAddCaps, TextRemove, TextConfirm, + TextInvalid, CaretMove, SelectCharacter, SelectWord, @@ -95,6 +96,7 @@ namespace osu.Game.Graphics.UserInterface { FeedbackSampleType.TextAddCaps, new[] { audio.Samples.Get(@"Keyboard/key-caps") } }, { FeedbackSampleType.TextRemove, new[] { audio.Samples.Get(@"Keyboard/key-delete") } }, { FeedbackSampleType.TextConfirm, new[] { audio.Samples.Get(@"Keyboard/key-confirm") } }, + { FeedbackSampleType.TextInvalid, new[] { audio.Samples.Get(@"Keyboard/key-invalid") } }, { FeedbackSampleType.CaretMove, new[] { audio.Samples.Get(@"Keyboard/key-movement") } }, { FeedbackSampleType.SelectCharacter, new[] { audio.Samples.Get(@"Keyboard/select-char") } }, { FeedbackSampleType.SelectWord, new[] { audio.Samples.Get(@"Keyboard/select-word") } }, @@ -111,6 +113,9 @@ namespace osu.Game.Graphics.UserInterface { base.OnUserTextAdded(added); + if (!added.Any(CanAddCharacter)) + return; + if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples) playSample(FeedbackSampleType.TextAddCaps); else @@ -124,6 +129,13 @@ namespace osu.Game.Graphics.UserInterface playSample(FeedbackSampleType.TextRemove); } + protected override void NotifyInputError() + { + base.NotifyInputError(); + + playSample(FeedbackSampleType.TextInvalid); + } + protected override void OnTextCommitted(bool textChanged) { base.OnTextCommitted(textChanged); From b5ec7d06dda29ec8463d9aac8cbe3987c7e9e93b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 31 Aug 2022 19:49:04 +0900 Subject: [PATCH 463/532] Add auto-skip setting Default to auto skip --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 5 ++++- .../Online/Multiplayer/MultiplayerRoomSettings.cs | 9 +++++++-- osu.Game/Online/Rooms/Room.cs | 5 +++++ .../Match/MultiplayerMatchSettingsOverlay.cs | 13 ++++++++++++- osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs | 3 +++ 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 04b87c19da..364309ffe4 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -265,8 +265,9 @@ namespace osu.Game.Online.Multiplayer /// The type of the match, if any. /// The new queue mode, if any. /// The new auto-start countdown duration, if any. + /// The new auto-skip setting. public Task ChangeSettings(Optional name = default, Optional password = default, Optional matchType = default, Optional queueMode = default, - Optional autoStartDuration = default) + Optional autoStartDuration = default, Optional autoSkip = default) { if (Room == null) throw new InvalidOperationException("Must be joined to a match to change settings."); @@ -278,6 +279,7 @@ namespace osu.Game.Online.Multiplayer MatchType = matchType.GetOr(Room.Settings.MatchType), QueueMode = queueMode.GetOr(Room.Settings.QueueMode), AutoStartDuration = autoStartDuration.GetOr(Room.Settings.AutoStartDuration), + AutoSkip = autoSkip.GetOr(Room.Settings.AutoSkip) }); } @@ -739,6 +741,7 @@ namespace osu.Game.Online.Multiplayer APIRoom.QueueMode.Value = Room.Settings.QueueMode; APIRoom.AutoStartDuration.Value = Room.Settings.AutoStartDuration; APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == settings.PlaylistItemId); + APIRoom.AutoSkip.Value = Room.Settings.AutoSkip; RoomUpdated?.Invoke(); } diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index 356acb427c..c73b02874e 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -29,6 +29,9 @@ namespace osu.Game.Online.Multiplayer [Key(5)] public TimeSpan AutoStartDuration { get; set; } + [Key(6)] + public bool AutoSkip { get; set; } + [IgnoreMember] public bool AutoStartEnabled => AutoStartDuration != TimeSpan.Zero; @@ -42,7 +45,8 @@ namespace osu.Game.Online.Multiplayer && PlaylistItemId == other.PlaylistItemId && MatchType == other.MatchType && QueueMode == other.QueueMode - && AutoStartDuration == other.AutoStartDuration; + && AutoStartDuration == other.AutoStartDuration + && AutoSkip == other.AutoSkip; } public override string ToString() => $"Name:{Name}" @@ -50,6 +54,7 @@ namespace osu.Game.Online.Multiplayer + $" Type:{MatchType}" + $" Item:{PlaylistItemId}" + $" Queue:{QueueMode}" - + $" Start:{AutoStartDuration}"; + + $" Start:{AutoStartDuration}" + + $" AutoSkip:{AutoSkip}"; } } diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 3a24fa02a8..33397a237f 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -159,6 +159,10 @@ namespace osu.Game.Online.Rooms set => MaxAttempts.Value = value; } + [Cached] + [JsonProperty("auto_skip")] + public readonly Bindable AutoSkip = new Bindable(true); + public Room() { Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue)); @@ -195,6 +199,7 @@ namespace osu.Game.Online.Rooms DifficultyRange.Value = other.DifficultyRange.Value; PlaylistItemStats.Value = other.PlaylistItemStats.Value; CurrentPlaylistItem.Value = other.CurrentPlaylistItem.Value; + AutoSkip.Value = other.AutoSkip.Value; if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value) Status.Value = new RoomStatusEnded(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 3aa879dde0..3d6127e8e7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -63,6 +63,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public MatchTypePicker TypePicker; public OsuEnumDropdown QueueModeDropdown; public OsuTextBox PasswordTextBox; + public OsuCheckbox AutoSkipCheckbox; public TriangleButton ApplyButton; public OsuSpriteText ErrorText; @@ -249,6 +250,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match LengthLimit = 255, }, }, + new Section("Other") + { + Child = AutoSkipCheckbox = new OsuCheckbox + { + LabelText = "Automatically skip the beatmap intro" + } + } } } }, @@ -343,6 +351,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Password.BindValueChanged(password => PasswordTextBox.Text = password.NewValue ?? string.Empty, true); QueueMode.BindValueChanged(mode => QueueModeDropdown.Current.Value = mode.NewValue, true); AutoStartDuration.BindValueChanged(duration => startModeDropdown.Current.Value = (StartMode)(int)duration.NewValue.TotalSeconds, true); + AutoSkip.BindValueChanged(autoSkip => AutoSkipCheckbox.Current.Value = autoSkip.NewValue, true); operationInProgress.BindTo(ongoingOperationTracker.InProgress); operationInProgress.BindValueChanged(v => @@ -390,7 +399,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match password: PasswordTextBox.Text, matchType: TypePicker.Current.Value, queueMode: QueueModeDropdown.Current.Value, - autoStartDuration: autoStartDuration) + autoStartDuration: autoStartDuration, + autoSkip: AutoSkipCheckbox.Current.Value) .ContinueWith(t => Schedule(() => { if (t.IsCompletedSuccessfully) @@ -406,6 +416,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match room.Password.Value = PasswordTextBox.Current.Value; room.QueueMode.Value = QueueModeDropdown.Current.Value; room.AutoStartDuration.Value = autoStartDuration; + room.AutoSkip.Value = AutoSkipCheckbox.Current.Value; if (int.TryParse(MaxParticipantsField.Text, out int max)) room.MaxParticipants.Value = max; diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 0bf5f2604c..50ad3228e5 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -86,6 +86,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected Bindable AutoStartDuration { get; private set; } + [Resolved(typeof(Room))] + protected Bindable AutoSkip { get; private set; } + [Resolved(CanBeNull = true)] private IBindable subScreenSelectedItem { get; set; } From c852c54055c17537cf91242f0ab970f41ba76f71 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 31 Aug 2022 19:50:16 +0900 Subject: [PATCH 464/532] Consume auto skip setting during play --- osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 3 ++- osu.Game/Screens/Play/PlayerLoader.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index a82da7b185..773e68162e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -61,7 +61,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { AllowPause = false, AllowRestart = false, - AllowSkipping = false, + AllowSkipping = room.AutoSkip.Value, + AutomaticallySkipIntro = room.AutoSkip.Value }) { this.users = users; diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index a9fcab063c..6373633b5a 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -363,7 +363,7 @@ namespace osu.Game.Screens.Play return; CurrentPlayer = createPlayer(); - CurrentPlayer.Configuration.AutomaticallySkipIntro = quickRestart; + CurrentPlayer.Configuration.AutomaticallySkipIntro |= quickRestart; CurrentPlayer.RestartCount = restartCount++; CurrentPlayer.RestartRequested = restartRequested; From 50e8052f0706d00cb9cb8fae11d140566acb1e92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 22:08:20 +0900 Subject: [PATCH 465/532] 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 bff3627af7..85857771a5 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 5fc69e475f..f757fd77b9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index f763e411be..9fcc3753eb 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -62,7 +62,7 @@ - + From 6af8143c8caed96fbfb18fcd02c226cb311a18bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 22:34:07 +0900 Subject: [PATCH 466/532] Fix typing of new setting to allow it to be visible to tools export --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index b48f0b4ccd..5e2a92e5e9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Mods }; [SettingSource("Metronome ticks", "Whether a metronome beat should play in the background")] - public BindableBool Metronome { get; } = new BindableBool(true); + public Bindable Metronome { get; } = new BindableBool(true); #region Constants From ba20044af499ea5555e1cfc61e91ab64630a0cad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Aug 2022 23:24:39 +0900 Subject: [PATCH 467/532] Fix missing nullability consideraition --- .../Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs index b845b85e1f..16d564f0ee 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs @@ -194,7 +194,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("notification arrived", () => notificationOverlay.Verify(n => n.Post(It.IsAny()), Times.Once)); - AddStep("run notification action", () => lastNotification.Activated()); + AddStep("run notification action", () => lastNotification.Activated?.Invoke()); AddAssert("overlay shown", () => overlay.State.Value == Visibility.Visible); AddAssert("is resumed", () => overlay.CurrentScreen is ScreenUIScale); From d70208fcf189506ccefcce7617e9534cfc5b9e5c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 1 Sep 2022 14:14:22 +0900 Subject: [PATCH 468/532] Default to off --- osu.Game/Online/Rooms/Room.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 33397a237f..adfd4c226a 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -161,7 +161,7 @@ namespace osu.Game.Online.Rooms [Cached] [JsonProperty("auto_skip")] - public readonly Bindable AutoSkip = new Bindable(true); + public readonly Bindable AutoSkip = new Bindable(); public Room() { From 148e487c025421ea8596fd6e817519ddc6f90b58 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Sep 2022 19:59:20 +0900 Subject: [PATCH 469/532] Add failing test of date submitted search failing --- .../SongSelect/TestSceneBeatmapCarousel.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index c3e485d56b..c9e63fa621 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -494,6 +494,43 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Something is selected", () => carousel.SelectedBeatmapInfo != null); } + [Test] + public void TestSortingDateSubmitted() + { + var sets = new List(); + const string zzz_string = "zzzzz"; + + AddStep("Populuate beatmap sets", () => + { + sets.Clear(); + + for (int i = 0; i < 20; i++) + { + var set = TestResources.CreateTestBeatmapSetInfo(5); + + if (i >= 2 && i < 10) + set.DateSubmitted = DateTimeOffset.Now.AddMinutes(i); + if (i < 5) + set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string); + + sets.Add(set); + } + }); + + loadBeatmaps(sets); + + AddStep("Sort by date submitted", () => carousel.Filter(new FilterCriteria { Sort = SortMode.DateSubmitted }, false)); + checkVisibleItemCount(diff: false, count: 8); + checkVisibleItemCount(diff: true, count: 5); + AddStep("Sort by date submitted and string", () => carousel.Filter(new FilterCriteria + { + Sort = SortMode.DateSubmitted, + SearchText = zzz_string + }, false)); + checkVisibleItemCount(diff: false, count: 3); + checkVisibleItemCount(diff: true, count: 5); + } + [Test] public void TestSorting() { From 15246236242e14304a6ae6df5a84f36403b67e81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Sep 2022 19:27:33 +0900 Subject: [PATCH 470/532] Fix back-to-front filter logic Was copied across from a place which was checking for `match` and applied verbatim to a place that was `filter`. Which are polar opposites. --- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 6c134a4ab8..9a4319c6b2 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -129,12 +129,13 @@ namespace osu.Game.Screens.Select.Carousel public override void Filter(FilterCriteria criteria) { base.Filter(criteria); - bool match = Items.All(i => i.Filtered.Value); - match &= criteria.Sort != SortMode.DateRanked || BeatmapSet?.DateRanked != null; - match &= criteria.Sort != SortMode.DateSubmitted || BeatmapSet?.DateSubmitted != null; + bool filtered = Items.All(i => i.Filtered.Value); - Filtered.Value = match; + filtered |= criteria.Sort == SortMode.DateRanked && BeatmapSet?.DateRanked == null; + filtered |= criteria.Sort == SortMode.DateSubmitted && BeatmapSet?.DateSubmitted == null; + + Filtered.Value = filtered; } public override string ToString() => BeatmapSet.ToString(); From a27743126639d51619c29b68654e5f50ab053a03 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 1 Sep 2022 21:10:36 +0900 Subject: [PATCH 471/532] Add has_replay and legacy_score_id to SoloScoreInfo --- osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index a8cedabd48..659b661ef0 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -18,7 +18,7 @@ namespace osu.Game.Online.API.Requests.Responses [Serializable] public class SoloScoreInfo : IHasOnlineID { - [JsonProperty("replay")] + [JsonProperty("has_replay")] public bool HasReplay { get; set; } [JsonProperty("beatmap_id")] @@ -83,6 +83,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("legacy_total_score")] public int? LegacyTotalScore { get; set; } + [JsonProperty("legacy_score_id")] + public uint? LegacyScoreId { get; set; } + #region osu-web API additions (not stored to database). [JsonProperty("id")] @@ -117,7 +120,6 @@ namespace osu.Game.Online.API.Requests.Responses public bool ShouldSerializeBeatmapSet() => false; public bool ShouldSerializePP() => false; public bool ShouldSerializeOnlineID() => false; - public bool ShouldSerializeHasReplay() => false; #endregion From 8866250cff5742c2604ab7b3532044ae3b6adcef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Sep 2022 21:42:38 +0900 Subject: [PATCH 472/532] Fix seasonal background not being unloaded when changing setting to "Never" Closes #20065. --- .../Backgrounds/SeasonalBackgroundLoader.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index 99af95b5fe..5c98e22818 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -38,21 +38,19 @@ namespace osu.Game.Graphics.Backgrounds private void load(OsuConfigManager config, SessionStatics sessionStatics) { seasonalBackgroundMode = config.GetBindable(OsuSetting.SeasonalBackgroundMode); - seasonalBackgroundMode.BindValueChanged(_ => triggerSeasonalBackgroundChanged()); + seasonalBackgroundMode.BindValueChanged(_ => SeasonalBackgroundChanged?.Invoke()); seasonalBackgrounds = sessionStatics.GetBindable(Static.SeasonalBackgrounds); - seasonalBackgrounds.BindValueChanged(_ => triggerSeasonalBackgroundChanged()); + seasonalBackgrounds.BindValueChanged(_ => + { + if (shouldShowSeasonal) + SeasonalBackgroundChanged?.Invoke(); + }); apiState.BindTo(api.State); apiState.BindValueChanged(fetchSeasonalBackgrounds, true); } - private void triggerSeasonalBackgroundChanged() - { - if (shouldShowSeasonal) - SeasonalBackgroundChanged?.Invoke(); - } - private void fetchSeasonalBackgrounds(ValueChangedEvent stateChanged) { if (seasonalBackgrounds.Value != null || stateChanged.NewValue != APIState.Online) From d3ae60ec6d38ce3dd55d588833a4edbfc8a7e274 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Sep 2022 22:02:58 +0900 Subject: [PATCH 473/532] Fix tournament population failure when beatmap is not found on server --- osu.Game.Tournament/Models/TournamentBeatmap.cs | 3 +-- osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Models/TournamentBeatmap.cs b/osu.Game.Tournament/Models/TournamentBeatmap.cs index 274fddc490..7f57b6a151 100644 --- a/osu.Game.Tournament/Models/TournamentBeatmap.cs +++ b/osu.Game.Tournament/Models/TournamentBeatmap.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Extensions.ObjectExtensions; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API.Requests.Responses; @@ -41,7 +40,7 @@ namespace osu.Game.Tournament.Models StarRating = beatmap.StarRating; Metadata = beatmap.Metadata; Difficulty = beatmap.Difficulty; - Covers = beatmap.BeatmapSet.AsNonNull().Covers; + Covers = beatmap.BeatmapSet?.Covers ?? new BeatmapSetOnlineCovers(); } public bool Equals(IBeatmapInfo? other) => other is TournamentBeatmap b && this.MatchesOnlineID(b); diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs b/osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs index b76496b145..aad31befa8 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using Newtonsoft.Json; namespace osu.Game.Beatmaps From 22c18d9a81cfdb1e777b09ff4bfb6fa42170be67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Sep 2022 23:03:28 +0900 Subject: [PATCH 474/532] 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 85857771a5..219912425f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f757fd77b9..43e3076f5c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 9fcc3753eb..0f0bf2848c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - + From c9dec473d3e4ef96d093da510b2b4f666512ed05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Sep 2022 23:05:07 +0900 Subject: [PATCH 475/532] Update virtual track logic to match framework changes --- osu.Game/Tests/Visual/OsuTestScene.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 5a297fd109..5055153691 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -404,9 +404,9 @@ namespace osu.Game.Tests.Visual public IEnumerable GetAvailableResources() => throw new NotImplementedException(); - public Track GetVirtual(double length = double.PositiveInfinity) + public Track GetVirtual(double length = double.PositiveInfinity, string name = "virtual") { - var track = new TrackVirtualManual(referenceClock) { Length = length }; + var track = new TrackVirtualManual(referenceClock, name) { Length = length }; AddItem(track); return track; } @@ -421,7 +421,8 @@ namespace osu.Game.Tests.Visual private bool running; - public TrackVirtualManual(IFrameBasedClock referenceClock) + public TrackVirtualManual(IFrameBasedClock referenceClock, string name = "virtual") + : base(name) { this.referenceClock = referenceClock; Length = double.PositiveInfinity; From 7eaa4c5ccd89ff88c691f9bddf7cc30eea8a64f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Sep 2022 23:05:34 +0900 Subject: [PATCH 476/532] Update new usages of `Remove` / `RemoveInternal` --- osu.Game/Overlays/NotificationOverlayToastTray.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 4417b5e0d0..c47a61eac1 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -126,13 +126,13 @@ namespace osu.Game.Overlays Debug.Assert(notification.Parent == toastFlow); // Temporarily remove from flow so we can animate the position off to the right. - toastFlow.Remove(notification); + toastFlow.Remove(notification, false); AddInternal(notification); notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint); notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ => { - RemoveInternal(notification); + RemoveInternal(notification, false); ForwardNotificationToPermanentStore?.Invoke(notification); notification.FadeIn(300, Easing.OutQuint); From 23d5e8b286af37ec6dd4c79e4b837a9df5d2c136 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Sep 2022 23:45:59 +0900 Subject: [PATCH 477/532] Fix beat sync components stopping after beatmap change Not an amazing fix, but it seems to work and would rather get this in ASAP rather than trying to fix at a framework level. Closes #20059. --- osu.Game/OsuGameBase.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 97142d5472..c95a281f09 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -390,6 +390,11 @@ namespace osu.Game var framedClock = new FramedClock(beatmap.Track); beatmapClock.ChangeSource(framedClock); + + // Normally the internal decoupled clock will seek the *track* to the decoupled time, but we blocked this. + // It won't behave nicely unless we also set it to the track's time. + // Probably another thing which should be fixed in the decoupled mess (or just replaced). + beatmapClock.Seek(beatmap.Track.CurrentTime); } protected virtual void InitialiseFonts() From d13e353a534a8ee93493f019d9640da8e2bb7183 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Sep 2022 03:02:27 +0900 Subject: [PATCH 478/532] Fix double colour application in update progress notification I'd like to restore it to yellow, but let's clean the slate first. --- osu.Desktop/Updater/SquirrelUpdateManager.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index d53db6c516..6f45237522 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -6,8 +6,6 @@ using System.Runtime.Versioning; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Logging; using osu.Game; @@ -15,7 +13,6 @@ using osu.Game.Graphics; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osuTK; -using osuTK.Graphics; using Squirrel; using Squirrel.SimpleSplat; @@ -177,17 +174,11 @@ namespace osu.Desktop.Updater { IconContent.AddRange(new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(colours.YellowDark, colours.Yellow) - }, new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.Upload, - Colour = Color4.White, Size = new Vector2(20), } }); From 9645bfe7085fccfd6c79156bf1e9378576545f82 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 2 Sep 2022 16:27:25 +0900 Subject: [PATCH 479/532] Bump difficulty calculator versions --- osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 5d30d33190..63e61f17e3 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty private readonly bool isForCurrentRuleset; private readonly double originalOverallDifficulty; - public override int Version => 20220701; + public override int Version => 20220902; public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 0ebfb9a283..6ef17d47c0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private const double difficulty_multiplier = 0.0675; private double hitWindowGreat; - public override int Version => 20220701; + public override int Version => 20220902; public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index ea2f04a3d9..2b0b563323 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { private const double difficulty_multiplier = 1.35; - public override int Version => 20220701; + public override int Version => 20220902; public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) : base(ruleset, beatmap) From a70fc10d06ffb1f03ee0aaf895a5eeb978da5333 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Sep 2022 16:30:04 +0900 Subject: [PATCH 480/532] Fix mock track construction failure --- osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs b/osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs index 9fdd49823e..50e6087526 100644 --- a/osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckAudioQualityTest.cs @@ -7,10 +7,12 @@ using System.Linq; using Moq; using NUnit.Framework; using osu.Framework.Audio.Track; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks; using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Visual; namespace osu.Game.Tests.Editing.Checks { @@ -109,7 +111,7 @@ namespace osu.Game.Tests.Editing.Checks /// The bitrate of the audio file the beatmap uses. private Mock getMockWorkingBeatmap(int? audioBitrate) { - var mockTrack = new Mock(); + var mockTrack = new Mock(new FramedClock(), "virtual"); mockTrack.SetupGet(t => t.Bitrate).Returns(audioBitrate); var mockWorkingBeatmap = new Mock(); From 8c50ccc48e973e86c2a3381d7fae69ae5e8c401a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Sep 2022 16:37:39 +0900 Subject: [PATCH 481/532] Fix incorrect specification in `SectionsContainer` --- osu.Game/Graphics/Containers/SectionsContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index b8a8ea79cc..583f1385c1 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Graphics.Containers if (value == expandableHeader) return; if (expandableHeader != null) - RemoveInternal(expandableHeader, false); + RemoveInternal(expandableHeader, true); expandableHeader = value; From b10026993ab4a9f2b55db30fc479a63c1a2f760d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 2 Sep 2022 16:40:32 +0900 Subject: [PATCH 482/532] Don't serialise has_replay --- osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index 659b661ef0..d7b97cdddf 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -18,9 +18,6 @@ namespace osu.Game.Online.API.Requests.Responses [Serializable] public class SoloScoreInfo : IHasOnlineID { - [JsonProperty("has_replay")] - public bool HasReplay { get; set; } - [JsonProperty("beatmap_id")] public int BeatmapID { get; set; } @@ -114,12 +111,16 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("pp")] public double? PP { get; set; } + [JsonProperty("has_replay")] + public bool HasReplay { get; set; } + public bool ShouldSerializeID() => false; public bool ShouldSerializeUser() => false; public bool ShouldSerializeBeatmap() => false; public bool ShouldSerializeBeatmapSet() => false; public bool ShouldSerializePP() => false; public bool ShouldSerializeOnlineID() => false; + public bool ShouldSerializeHasReplay() => false; #endregion From 65baf73d97b3e4b4bfcc22ffb5a1971054c306f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Sep 2022 16:50:28 +0900 Subject: [PATCH 483/532] Add test scene --- .../Editing/TestSceneDifficultyDelete.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs new file mode 100644 index 0000000000..ef0c91cd90 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs @@ -0,0 +1,67 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Storyboards; +using osu.Game.Tests.Beatmaps.IO; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneDifficultyDelete : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + protected override bool IsolateSavingFromDatabase => false; + + [Resolved] + private OsuGameBase game { get; set; } = null!; + + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + private BeatmapSetInfo importedBeatmapSet = null!; + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null!) + => beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First()); + + public override void SetUpSteps() + { + AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely()); + base.SetUpSteps(); + } + + [Test] + public void TestDifficultyDelete() + { + string lastDiff = null!; + AddStep("remember selected difficulty", () => lastDiff = EditorBeatmap.BeatmapInfo.DifficultyName); + + AddStep("click File", () => this.ChildrenOfType().First().TriggerClick()); + AddStep("click Delete", () => this.ChildrenOfType().Single(deleteMenuItemPredicate).TriggerClick()); + AddStep("confirm", () => InputManager.Key(Key.Number2)); + + AddAssert("difficulty is deleted", () => + { + if (lastDiff == null!) throw new NullReferenceException(); + + var newSet = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(), true).BeatmapSetInfo; + return newSet.Beatmaps.All(x => x.DifficultyName != lastDiff); + }); + } + + private bool deleteMenuItemPredicate(DrawableOsuMenuItem item) + { + return item.ChildrenOfType().Any(text => text.Text.ToString().StartsWith("Delete", StringComparison.Ordinal)); + } + } +} From 605108c938486b276a6b4c6806ccda2140771b81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Sep 2022 16:58:46 +0900 Subject: [PATCH 484/532] Refactor/rename deletion method to read better --- osu.Game/Beatmaps/BeatmapManager.cs | 31 ++++++++++++++--------------- osu.Game/Screens/Edit/Editor.cs | 2 +- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 2baac05d8b..d784f1e627 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -364,27 +364,26 @@ namespace osu.Game.Beatmaps } /// - /// Hard-Delete a beatmap difficulty locally. + /// Delete a beatmap difficulty immediately. /// - /// is generally preferred as it keeps the local beatmap set in sync with the server. - /// The beatmap difficulty to hide. - public void DeleteLocal(BeatmapInfo beatmapInfo) + /// + /// There's no undoing this operation, as we don't have a soft-deletion flag on . + /// This may be a future consideration if there's a user requirement for undeleting support. + /// + public void DeleteDifficultyImmediately(BeatmapInfo beatmapInfo) { - Realm.Run(r => + Realm.Write(r => { - using (var transaction = r.BeginWrite()) - { - if (!beatmapInfo.IsManaged) - beatmapInfo = r.Find(beatmapInfo.ID); + if (!beatmapInfo.IsManaged) + beatmapInfo = r.Find(beatmapInfo.ID); - var setInfo = beatmapInfo.BeatmapSet!; - DeleteFile(setInfo, beatmapInfo.File!); - setInfo.Beatmaps.Remove(beatmapInfo); + Debug.Assert(beatmapInfo.BeatmapSet != null); + Debug.Assert(beatmapInfo.File != null); - var liveSetInfo = r.Find(setInfo.ID); - setInfo.CopyChangesToRealm(liveSetInfo); - transaction.Commit(); - } + var setInfo = beatmapInfo.BeatmapSet; + + DeleteFile(setInfo, beatmapInfo.File); + setInfo.Beatmaps.Remove(beatmapInfo); }); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 10a86cdca3..24a89d124d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -904,7 +904,7 @@ namespace osu.Game.Screens.Edit var current = playableBeatmap.BeatmapInfo; if (current is null) return; - beatmapManager.DeleteLocal(current); + beatmapManager.DeleteDifficultyImmediately(current); switchBeatmapOrExit(current.BeatmapSet); } From 9fd8067e11d4d21d39dc9e33b1e3b9df39647519 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Sep 2022 17:07:09 +0900 Subject: [PATCH 485/532] Tidy up dialog and deletion flow --- .../DeleteDifficultyConfirmationDialog.cs | 18 +++++++ osu.Game/Screens/Edit/Editor.cs | 52 ++++++++----------- .../Edit/PromptForDifficultyDeleteDialog.cs | 38 -------------- 3 files changed, 41 insertions(+), 67 deletions(-) create mode 100644 osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs delete mode 100644 osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs diff --git a/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs b/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs new file mode 100644 index 0000000000..594042b426 --- /dev/null +++ b/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Beatmaps; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Edit +{ + public class DeleteDifficultyConfirmationDialog : DeleteConfirmationDialog + { + public DeleteDifficultyConfirmationDialog(BeatmapInfo beatmapInfo, Action deleteAction) + { + BodyText = $"\"{beatmapInfo.DifficultyName}\" difficulty"; + DeleteAction = deleteAction; + } + } +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 24a89d124d..2e09b9a865 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -879,35 +879,6 @@ namespace osu.Game.Screens.Edit clock.SeekForward(!trackPlaying, amount); } - private void exportBeatmap() - { - Save(); - new LegacyBeatmapExporter(storage).Export(Beatmap.Value.BeatmapSetInfo); - } - - private void deleteDifficulty() - { - dialogOverlay?.Push(new PromptForDifficultyDeleteDialog(confirmDifficultyHide, confirmDifficultyDelete, () => { })); - } - - private void confirmDifficultyHide() - { - var current = playableBeatmap.BeatmapInfo; - if (current is null) return; - - beatmapManager.Hide(current); - switchBeatmapOrExit(current.BeatmapSet); - } - - private void confirmDifficultyDelete() - { - var current = playableBeatmap.BeatmapInfo; - if (current is null) return; - - beatmapManager.DeleteDifficultyImmediately(current); - switchBeatmapOrExit(current.BeatmapSet); - } - private void switchBeatmapOrExit([CanBeNull] BeatmapSetInfo setInfo) { if (setInfo is null || setInfo.Beatmaps.Count() <= 1) @@ -948,6 +919,29 @@ namespace osu.Game.Screens.Edit return fileMenuItems; } + private void exportBeatmap() + { + Save(); + new LegacyBeatmapExporter(storage).Export(Beatmap.Value.BeatmapSetInfo); + } + + private void deleteDifficulty() + { + if (dialogOverlay == null) + delete(); + else + dialogOverlay.Push(new DeleteDifficultyConfirmationDialog(Beatmap.Value.BeatmapInfo, delete)); + + void delete() + { + var current = playableBeatmap.BeatmapInfo; + if (current is null) return; + + beatmapManager.DeleteDifficultyImmediately(current); + switchBeatmapOrExit(current.BeatmapSet); + } + } + private EditorMenuItem createDifficultyCreationMenu() { var rulesetItems = new List(); diff --git a/osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs b/osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs deleted file mode 100644 index dcea9f1210..0000000000 --- a/osu.Game/Screens/Edit/PromptForDifficultyDeleteDialog.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Graphics.Sprites; -using osu.Game.Overlays.Dialog; - -namespace osu.Game.Screens.Edit -{ - public class PromptForDifficultyDeleteDialog : PopupDialog - { - public PromptForDifficultyDeleteDialog(Action hide, Action delete, Action cancel) - { - HeaderText = "Are you sure you want to delete this difficulty?"; - - Icon = FontAwesome.Regular.TrashAlt; - - Buttons = new PopupDialogButton[] - { - new PopupDialogOkButton - { - Text = @"Hide this difficulty instead (recommended)", - Action = hide - }, - new PopupDialogDangerousButton - { - Text = @"Yes, DELETE this difficulty!", - Action = delete - }, - new PopupDialogCancelButton - { - Text = @"Oops, continue editing", - Action = cancel - }, - }; - } - } -} From 840d1c4cd59aefa26e0d3cb8e6aa8b0961e0aa51 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Sep 2022 17:14:48 +0900 Subject: [PATCH 486/532] Disable delete difficulty menu item when only one difficulty is present --- osu.Game/Screens/Edit/Editor.cs | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 2e09b9a865..5df403e643 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -895,29 +895,18 @@ namespace osu.Game.Screens.Edit lastSavedHash = changeHandler?.CurrentStateHash; } - private List createFileMenuItems() + private List createFileMenuItems() => new List { - var fileMenuItems = new List - { - new EditorMenuItem("Save", MenuItemType.Standard, () => Save()) - }; - - if (RuntimeInfo.IsDesktop) - fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap)); - - fileMenuItems.Add(new EditorMenuItemSpacer()); - - fileMenuItems.Add(createDifficultyCreationMenu()); - fileMenuItems.Add(createDifficultySwitchMenu()); - - fileMenuItems.Add(new EditorMenuItemSpacer()); - - fileMenuItems.Add(new EditorMenuItem("Delete difficulty", MenuItemType.Standard, deleteDifficulty)); - - fileMenuItems.Add(new EditorMenuItemSpacer()); - fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)); - return fileMenuItems; - } + new EditorMenuItem("Save", MenuItemType.Standard, () => Save()), + new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new EditorMenuItemSpacer(), + createDifficultyCreationMenu(), + createDifficultySwitchMenu(), + new EditorMenuItemSpacer(), + new EditorMenuItem("Delete difficulty", MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } }, + new EditorMenuItemSpacer(), + new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit) + }; private void exportBeatmap() { From 4f18105e9d9045a1cd1c4ae98d30997de73b4588 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Sep 2022 17:39:14 +0900 Subject: [PATCH 487/532] Ensure next beatmap selected matches the menu ordering --- osu.Game/Screens/Edit/Editor.cs | 44 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 5df403e643..6e06683e38 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework; @@ -879,17 +878,6 @@ namespace osu.Game.Screens.Edit clock.SeekForward(!trackPlaying, amount); } - private void switchBeatmapOrExit([CanBeNull] BeatmapSetInfo setInfo) - { - if (setInfo is null || setInfo.Beatmaps.Count() <= 1) - this.Exit(); - var nextBeatmap = setInfo!.Beatmaps.First(); - - // Force a refresh of the beatmap (and beatmap set) so the `Change difficulty` list is also updated. - Beatmap.Value = beatmapManager.GetWorkingBeatmap(nextBeatmap, true); - SwitchToDifficulty(Beatmap.Value.BeatmapInfo); - } - private void updateLastSavedHash() { lastSavedHash = changeHandler?.CurrentStateHash; @@ -914,6 +902,14 @@ namespace osu.Game.Screens.Edit new LegacyBeatmapExporter(storage).Export(Beatmap.Value.BeatmapSetInfo); } + /// + /// Beatmaps of the currently edited set, grouped by ruleset and ordered by difficulty. + /// + private IOrderedEnumerable> groupedOrderedBeatmaps => Beatmap.Value.BeatmapSetInfo.Beatmaps + .OrderBy(b => b.StarRating) + .GroupBy(b => b.Ruleset) + .OrderBy(group => group.Key); + private void deleteDifficulty() { if (dialogOverlay == null) @@ -923,11 +919,19 @@ namespace osu.Game.Screens.Edit void delete() { - var current = playableBeatmap.BeatmapInfo; - if (current is null) return; + BeatmapInfo difficultyToDelete = playableBeatmap.BeatmapInfo; - beatmapManager.DeleteDifficultyImmediately(current); - switchBeatmapOrExit(current.BeatmapSet); + var difficultiesBeforeDeletion = groupedOrderedBeatmaps.SelectMany(g => g).ToList(); + + beatmapManager.DeleteDifficultyImmediately(difficultyToDelete); + + int deletedIndex = difficultiesBeforeDeletion.IndexOf(difficultyToDelete); + // of note, we're still working with the cloned version, so indices are all prior to deletion. + BeatmapInfo nextToShow = difficultiesBeforeDeletion[deletedIndex == 0 ? 1 : deletedIndex - 1]; + + Beatmap.Value = beatmapManager.GetWorkingBeatmap(nextToShow, true); + + SwitchToDifficulty(nextToShow); } } @@ -960,18 +964,14 @@ namespace osu.Game.Screens.Edit private EditorMenuItem createDifficultySwitchMenu() { - var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet; - - Debug.Assert(beatmapSet != null); - var difficultyItems = new List(); - foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.Ruleset).OrderBy(group => group.Key)) + foreach (var rulesetBeatmaps in groupedOrderedBeatmaps) { if (difficultyItems.Count > 0) difficultyItems.Add(new EditorMenuItemSpacer()); - foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarRating)) + foreach (var beatmap in rulesetBeatmaps) { bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmap); difficultyItems.Add(new DifficultyMenuItem(beatmap, isCurrentDifficulty, SwitchToDifficulty)); From dc02b59a05cd76f224bc9c96535741c72a3342a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Sep 2022 17:57:19 +0900 Subject: [PATCH 488/532] Add extra coverage to tests Also fixes a potential failure due to test beatmap having two difficulties with same name. --- .../Editing/TestSceneDifficultyDelete.cs | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs index ef0c91cd90..1520f64ec0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs @@ -41,27 +41,42 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestDifficultyDelete() + public void TestDeleteDifficulties() { - string lastDiff = null!; - AddStep("remember selected difficulty", () => lastDiff = EditorBeatmap.BeatmapInfo.DifficultyName); + Guid deletedDifficultyID = Guid.Empty; + int countBeforeDeletion = 0; - AddStep("click File", () => this.ChildrenOfType().First().TriggerClick()); - AddStep("click Delete", () => this.ChildrenOfType().Single(deleteMenuItemPredicate).TriggerClick()); - AddStep("confirm", () => InputManager.Key(Key.Number2)); - - AddAssert("difficulty is deleted", () => + for (int i = 0; i < 12; i++) { - if (lastDiff == null!) throw new NullReferenceException(); + // Will be reloaded after each deletion. + AddUntilStep("wait for editor to load", () => Editor?.ReadyForUse == true); - var newSet = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(), true).BeatmapSetInfo; - return newSet.Beatmaps.All(x => x.DifficultyName != lastDiff); - }); + AddStep("store selected difficulty", () => + { + deletedDifficultyID = EditorBeatmap.BeatmapInfo.ID; + countBeforeDeletion = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count; + }); + + AddStep("click File", () => this.ChildrenOfType().First().TriggerClick()); + + if (i == 11) + { + // last difficulty shouldn't be able to be deleted. + AddAssert("Delete menu item disabled", () => getDeleteMenuItem().Item.Action.Disabled); + } + else + { + AddStep("click delete", () => getDeleteMenuItem().TriggerClick()); + AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null); + AddStep("confirm", () => InputManager.Key(Key.Number1)); + + AddAssert($"difficulty {i} is deleted", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Select(b => b.ID), () => Does.Not.Contain(deletedDifficultyID)); + AddAssert("count decreased by one", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, () => Is.EqualTo(countBeforeDeletion - 1)); + } + } } - private bool deleteMenuItemPredicate(DrawableOsuMenuItem item) - { - return item.ChildrenOfType().Any(text => text.Text.ToString().StartsWith("Delete", StringComparison.Ordinal)); - } + private DrawableOsuMenuItem getDeleteMenuItem() => this.ChildrenOfType() + .Single(item => item.ChildrenOfType().Any(text => text.Text.ToString().StartsWith("Delete", StringComparison.Ordinal))); } } From 8bfaa2a51f09e21a64db332b5de1716aa12e9dd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Sep 2022 18:14:47 +0900 Subject: [PATCH 489/532] Fix tournament match handling right click to select itself, dismissing the context menu --- .../Screens/Ladder/Components/DrawableTournamentMatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index 5204edf3be..ed8b789387 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -280,7 +280,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components protected override bool OnClick(ClickEvent e) { - if (editorInfo == null || Match is ConditionalTournamentMatch) + if (editorInfo == null || Match is ConditionalTournamentMatch || e.Button != MouseButton.Left) return false; Selected = true; From 8d6739ae73b5eb13b07ee29c02a11a7a20547267 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Sep 2022 18:20:23 +0900 Subject: [PATCH 490/532] Show team scores at the tournament map pool screen --- .../Screens/Gameplay/Components/TeamScoreDisplay.cs | 5 +++++ osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs index 5ee57e9271..0fa5884603 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs @@ -25,6 +25,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components public bool ShowScore { + get => teamDisplay.ShowScore; set => teamDisplay.ShowScore = value; } @@ -92,10 +93,14 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private void teamChanged(ValueChangedEvent team) { + bool wasShowingScores = teamDisplay?.ShowScore ?? false; + InternalChildren = new Drawable[] { teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), }; + + teamDisplay.ShowScore = wasShowingScores; } } } diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 4d36515316..decd723814 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -46,7 +46,10 @@ namespace osu.Game.Tournament.Screens.MapPool Loop = true, RelativeSizeAxes = Axes.Both, }, - new MatchHeader(), + new MatchHeader + { + ShowScores = true, + }, mapFlows = new FillFlowContainer> { Y = 160, From 778d767a1288fe8daaa1c814f9b42bad62283dae Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 3 Sep 2022 15:02:56 +0300 Subject: [PATCH 491/532] Revert disposal on `SectionsContainer` properties --- osu.Game/Graphics/Containers/SectionsContainer.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 583f1385c1..97e9ff88b5 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Graphics.Containers if (value == expandableHeader) return; if (expandableHeader != null) - RemoveInternal(expandableHeader, true); + RemoveInternal(expandableHeader, false); expandableHeader = value; @@ -55,6 +55,7 @@ namespace osu.Game.Graphics.Containers fixedHeader?.Expire(); fixedHeader = value; + if (value == null) return; AddInternal(fixedHeader); @@ -70,8 +71,10 @@ namespace osu.Game.Graphics.Containers if (value == footer) return; if (footer != null) - scrollContainer.Remove(footer, true); + scrollContainer.Remove(footer, false); + footer = value; + if (value == null) return; footer.Anchor |= Anchor.y2; From b43995269ad724ecf2ee352bb147fa3b363872d8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 3 Sep 2022 15:17:46 +0300 Subject: [PATCH 492/532] Dispose `ScrollingTeam`s on removal --- .../Screens/Drawings/Components/ScrollingTeamContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs index 55555adb80..8092c24ccb 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs @@ -170,7 +170,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components availableTeams.Add(team); - RemoveAll(c => c is ScrollingTeam, false); + RemoveAll(c => c is ScrollingTeam, true); setScrollState(ScrollState.Idle); } From e8fa872f61e08ddd88488a3bd7efd1e0b3fab6bb Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 3 Sep 2022 16:14:21 +0300 Subject: [PATCH 493/532] Fix room status dropdown position inconsistent on online-play screens --- .../OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 37b977cff7..8206d4b64d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer roomAccessTypeDropdown.Current.BindValueChanged(_ => UpdateFilter()); - return base.CreateFilterControls().Prepend(roomAccessTypeDropdown); + return base.CreateFilterControls().Append(roomAccessTypeDropdown); } protected override FilterCriteria CreateFilterCriteria() From 8cbd3443308f08941e7b670516ed25355665e948 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 5 Sep 2022 11:28:12 +0900 Subject: [PATCH 494/532] Improve performance when cancelling import with debugger attached --- .../Database/RealmArchiveModelImporter.cs | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index b340d0ee4b..f1bc0bfe0e 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -108,47 +108,42 @@ namespace osu.Game.Database bool isBatchImport = tasks.Length >= minimum_items_considered_batch_import; - try + await Task.WhenAll(tasks.Select(async task => { - await Task.WhenAll(tasks.Select(async task => + if (notification.CancellationToken.IsCancellationRequested) + return; + + try { - notification.CancellationToken.ThrowIfCancellationRequested(); + var model = await Import(task, isBatchImport, notification.CancellationToken).ConfigureAwait(false); - try + lock (imported) { - var model = await Import(task, isBatchImport, notification.CancellationToken).ConfigureAwait(false); + if (model != null) + imported.Add(model); + current++; - lock (imported) - { - if (model != null) - imported.Add(model); - current++; + notification.Text = $"Imported {current} of {tasks.Length} {HumanisedModelName}s"; + notification.Progress = (float)current / tasks.Length; + } + } + catch (OperationCanceledException) + { + } + catch (Exception e) + { + Logger.Error(e, $@"Could not import ({task})", LoggingTarget.Database); + } + })).ConfigureAwait(false); - notification.Text = $"Imported {current} of {tasks.Length} {HumanisedModelName}s"; - notification.Progress = (float)current / tasks.Length; - } - } - catch (TaskCanceledException) - { - throw; - } - catch (Exception e) - { - Logger.Error(e, $@"Could not import ({task})", LoggingTarget.Database); - } - })).ConfigureAwait(false); - } - catch (OperationCanceledException) + if (imported.Count == 0) { - if (imported.Count == 0) + if (notification.CancellationToken.IsCancellationRequested) { notification.State = ProgressNotificationState.Cancelled; return imported; } - } - if (imported.Count == 0) - { notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import failed!"; notification.State = ProgressNotificationState.Cancelled; } From 57954bb8f51382476d7d1dfbbcb0943431a5ec63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Sep 2022 14:50:43 +0900 Subject: [PATCH 495/532] Enable nullability on `TimelineHitObjectBlueprint` --- .../Timeline/TimelineHitObjectBlueprint.cs | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 32ebc9c3c1..974e240552 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -33,19 +31,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { private const float circle_size = 38; - private Container repeatsContainer; + private Container? repeatsContainer; - public Action OnDragHandled; + public Action? OnDragHandled = null!; [UsedImplicitly] private readonly Bindable startTime; - private Bindable indexInCurrentComboBindable; + private Bindable? indexInCurrentComboBindable; - private Bindable comboIndexBindable; - private Bindable comboIndexWithOffsetsBindable; + private Bindable? comboIndexBindable; + private Bindable? comboIndexWithOffsetsBindable; - private Bindable displayColourBindable; + private Bindable displayColourBindable = null!; private readonly ExtendableCircle circle; private readonly Border border; @@ -54,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly OsuSpriteText comboIndexText; [Resolved] - private ISkinSource skin { get; set; } + private ISkinSource skin { get; set; } = null!; public TimelineHitObjectBlueprint(HitObject item) : base(item) @@ -124,7 +122,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline case IHasComboInformation comboInfo: indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy(); - indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true); + indexInCurrentComboBindable.BindValueChanged(_ => + { + comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString(); + }, true); comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy(); comboIndexWithOffsetsBindable = comboInfo.ComboIndexWithOffsetsBindable.GetBoundCopy(); @@ -149,8 +150,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline updateColour(); } - private void updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString(); - private void updateColour() { Color4 colour; @@ -183,11 +182,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline colouredComponents.Colour = OsuColour.ForegroundTextColourFor(averageColour); } - private SamplePointPiece sampleOverrideDisplay; - private DifficultyPointPiece difficultyOverrideDisplay; + private SamplePointPiece? sampleOverrideDisplay; + private DifficultyPointPiece? difficultyOverrideDisplay; - private DifficultyControlPoint difficultyControlPoint; - private SampleControlPoint sampleControlPoint; + private DifficultyControlPoint difficultyControlPoint = null!; + private SampleControlPoint sampleControlPoint = null!; protected override void Update() { @@ -276,16 +275,27 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public class DragArea : Circle { - private readonly HitObject hitObject; + private readonly HitObject? hitObject; [Resolved] - private Timeline timeline { get; set; } + private EditorBeatmap beatmap { get; set; } = null!; - public Action OnDragHandled; + [Resolved] + private IBeatSnapProvider beatSnapProvider { get; set; } = null!; + + [Resolved] + private Timeline timeline { get; set; } = null!; + + [Resolved(CanBeNull = true)] + private IEditorChangeHandler? changeHandler { get; set; } + + private ScheduledDelegate? dragOperation; + + public Action? OnDragHandled; public override bool HandlePositionalInput => hitObject != null; - public DragArea(HitObject hitObject) + public DragArea(HitObject? hitObject) { this.hitObject = hitObject; @@ -356,23 +366,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline this.FadeTo(IsHovered || hasMouseDown ? 1f : 0.9f, 200, Easing.OutQuint); } - [Resolved] - private EditorBeatmap beatmap { get; set; } - - [Resolved] - private IBeatSnapProvider beatSnapProvider { get; set; } - - [Resolved(CanBeNull = true)] - private IEditorChangeHandler changeHandler { get; set; } - protected override bool OnDragStart(DragStartEvent e) { changeHandler?.BeginChange(); return true; } - private ScheduledDelegate dragOperation; - protected override void OnDrag(DragEvent e) { base.OnDrag(e); From 8af8adf22d1359673e4992d81cef7604e518685c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Sep 2022 16:17:26 +0900 Subject: [PATCH 496/532] Fix incorrect slider length in timeline when non-default velocity is inherited from previous object --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 59be93530c..4857f13a64 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -89,6 +89,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint(); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); + + // Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation. + // Without re-applying defaults, velocity won't be updated. + // If this causes further issues, it may be better to copy the velocity p + ApplyDefaultsToHitObject(); break; case SliderPlacementState.Body: From 2bec8b82b355ac78be961b235e620d341a63bdc9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Sep 2022 16:26:41 +0900 Subject: [PATCH 497/532] Fix textbox sample playback potentially crashing if called before load --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index e5341cfd4b..4c2e00d6e0 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -283,7 +283,7 @@ namespace osu.Game.Graphics.UserInterface return samples[RNG.Next(0, samples.Length)]?.GetChannel(); } - private void playSample(FeedbackSampleType feedbackSample) + private void playSample(FeedbackSampleType feedbackSample) => Schedule(() => { if (Time.Current < sampleLastPlaybackTime + 15) return; @@ -300,7 +300,7 @@ namespace osu.Game.Graphics.UserInterface channel.Play(); sampleLastPlaybackTime = Time.Current; - } + }); private class OsuCaret : Caret { From f1d9b225a7ddc43427ecf825910e2e24e18a5f79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Sep 2022 16:40:10 +0900 Subject: [PATCH 498/532] Remove probably pointless comment --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 4857f13a64..e2f98c273e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -92,7 +92,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders // Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation. // Without re-applying defaults, velocity won't be updated. - // If this causes further issues, it may be better to copy the velocity p ApplyDefaultsToHitObject(); break; From 4fa6707bf08c6f347c732c1de9b322e1fd0e0020 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Sep 2022 00:58:10 +0900 Subject: [PATCH 499/532] Set all progress notifications to non-important --- osu.Game/Overlays/Notifications/ProgressNotification.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 14cf6b3013..6821c25ac0 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -162,6 +162,8 @@ namespace osu.Game.Overlays.Notifications public override bool DisplayOnTop => false; + public override bool IsImportant => false; + private readonly ProgressBar progressBar; private Color4 colourQueued; private Color4 colourActive; From 56886fed09f87ad71c7c4bbd9b110d8cfed97e7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Sep 2022 00:58:27 +0900 Subject: [PATCH 500/532] Add test coverage of progress notification completions showing --- .../TestSceneNotificationOverlay.cs | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 38eecaa052..288f1f78bc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -23,9 +23,12 @@ namespace osu.Game.Tests.Visual.UserInterface private SpriteText displayedCount = null!; + public double TimeToCompleteProgress { get; set; } = 2000; + [SetUp] public void SetUp() => Schedule(() => { + TimeToCompleteProgress = 2000; progressingNotifications.Clear(); Content.Children = new Drawable[] @@ -45,6 +48,7 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestCompleteProgress() { ProgressNotification notification = null!; + AddStep("add progress notification", () => { notification = new ProgressNotification @@ -57,6 +61,30 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddUntilStep("wait completion", () => notification.State == ProgressNotificationState.Completed); + + AddAssert("Completion toast shown", () => notificationOverlay.ToastCount == 1); + } + + [Test] + public void TestCompleteProgressSlow() + { + ProgressNotification notification = null!; + + AddStep("Set progress slow", () => TimeToCompleteProgress *= 2); + AddStep("add progress notification", () => + { + notification = new ProgressNotification + { + Text = @"Uploading to BSS...", + CompletionText = "Uploaded to BSS!", + }; + notificationOverlay.Post(notification); + progressingNotifications.Add(notification); + }); + + AddUntilStep("wait completion", () => notification.State == ProgressNotificationState.Completed); + + AddAssert("Completion toast shown", () => notificationOverlay.ToastCount == 1); } [Test] @@ -177,7 +205,7 @@ namespace osu.Game.Tests.Visual.UserInterface foreach (var n in progressingNotifications.FindAll(n => n.State == ProgressNotificationState.Active)) { if (n.Progress < 1) - n.Progress += (float)(Time.Elapsed / 2000); + n.Progress += (float)(Time.Elapsed / TimeToCompleteProgress); else n.State = ProgressNotificationState.Completed; } From eca7b8f9884284d53d506c28b1ceadfedbffdfd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Sep 2022 00:51:59 +0900 Subject: [PATCH 501/532] Fix completion notifications not always showing as toasts --- osu.Game/Overlays/NotificationOverlay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 2c39ebcc87..fca43e65a5 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -137,7 +137,9 @@ namespace osu.Game.Overlays private readonly Scheduler postScheduler = new Scheduler(); - public override bool IsPresent => base.IsPresent || postScheduler.HasPendingTasks; + public override bool IsPresent => base.IsPresent + || postScheduler.HasPendingTasks + || toastTray.IsDisplayingToasts; private bool processingPosts = true; From 0d4ee6bd80ca8e8004de73f26b56f5695297b906 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Sep 2022 18:35:02 +0900 Subject: [PATCH 502/532] Centralise ability to fetch all toast tray notifications (including animating ones) --- osu.Game/Overlays/NotificationOverlayToastTray.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index c47a61eac1..aa61e20a02 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.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.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -23,7 +24,7 @@ namespace osu.Game.Overlays /// public class NotificationOverlayToastTray : CompositeDrawable { - public bool IsDisplayingToasts => toastFlow.Count > 0; + public bool IsDisplayingToasts => displayedCount > 0; private FillFlowContainer toastFlow = null!; private BufferedContainer toastContentBackground = null!; @@ -33,11 +34,14 @@ namespace osu.Game.Overlays public Action? ForwardNotificationToPermanentStore { get; set; } - public int UnreadCount => toastFlow.Count(n => !n.WasClosed && !n.Read) - + InternalChildren.OfType().Count(n => !n.WasClosed && !n.Read); + public int UnreadCount => allNotifications.Count(n => !n.WasClosed && !n.Read); + + private IEnumerable allNotifications => toastFlow.Concat(InternalChildren.OfType()); private int runningDepth; + private int displayedCount; + [BackgroundDependencyLoader] private void load() { @@ -92,6 +96,7 @@ namespace osu.Game.Overlays public void Post(Notification notification) { ++runningDepth; + displayedCount++; int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; @@ -133,6 +138,7 @@ namespace osu.Game.Overlays notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ => { RemoveInternal(notification, false); + displayedCount--; ForwardNotificationToPermanentStore?.Invoke(notification); notification.FadeIn(300, Easing.OutQuint); @@ -144,7 +150,7 @@ namespace osu.Game.Overlays base.Update(); float height = toastFlow.DrawHeight + 120; - float alpha = IsDisplayingToasts ? MathHelper.Clamp(toastFlow.DrawHeight / 40, 0, 1) * toastFlow.Children.Max(n => n.Alpha) : 0; + float alpha = toastFlow.Count > 0 ? MathHelper.Clamp(toastFlow.DrawHeight / 41, 0, 1) * toastFlow.Children.Max(n => n.Alpha) : 0; toastContentBackground.Height = (float)Interpolation.DampContinuously(toastContentBackground.Height, height, 10, Clock.ElapsedFrameTime); toastContentBackground.Alpha = (float)Interpolation.DampContinuously(toastContentBackground.Alpha, alpha, 10, Clock.ElapsedFrameTime); From f754686521f4262c7194e2503857b351b796fb64 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Sep 2022 18:57:33 +0900 Subject: [PATCH 503/532] Remove necessity of `AlwaysPresent` for `ProgressUpdate` completion posting --- osu.Game/Overlays/NotificationOverlay.cs | 7 +++---- osu.Game/Overlays/NotificationOverlayToastTray.cs | 2 +- .../Overlays/Notifications/ProgressNotification.cs | 13 ++++++------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index fca43e65a5..b170ea5dfa 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -71,7 +71,6 @@ namespace osu.Game.Overlays }, mainContent = new Container { - AlwaysPresent = true, RelativeSizeAxes = Axes.Both, Children = new Drawable[] { @@ -137,9 +136,9 @@ namespace osu.Game.Overlays private readonly Scheduler postScheduler = new Scheduler(); - public override bool IsPresent => base.IsPresent - || postScheduler.HasPendingTasks - || toastTray.IsDisplayingToasts; + public override bool IsPresent => + // Delegate presence as we need to consider the toast tray in addition to the main overlay. + State.Value == Visibility.Visible || mainContent.IsPresent || toastTray.IsPresent || postScheduler.HasPendingTasks; private bool processingPosts = true; diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index aa61e20a02..ebf8fb32a9 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -70,7 +70,7 @@ namespace osu.Game.Overlays postEffectDrawable.AutoSizeAxes = Axes.None; postEffectDrawable.RelativeSizeAxes = Axes.X; })), - toastFlow = new AlwaysUpdateFillFlowContainer + toastFlow = new FillFlowContainer { LayoutDuration = 150, LayoutEasing = Easing.OutQuart, diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 6821c25ac0..b5af3a56a1 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -88,7 +88,12 @@ namespace osu.Game.Overlays.Notifications state = value; if (IsLoaded) + { Schedule(updateState); + + if (state == ProgressNotificationState.Completed) + CompletionTarget?.Invoke(CreateCompletionNotification()); + } } } @@ -141,7 +146,7 @@ namespace osu.Game.Overlays.Notifications case ProgressNotificationState.Completed: loadingSpinner.Hide(); - Completed(); + base.Close(); break; } } @@ -154,12 +159,6 @@ namespace osu.Game.Overlays.Notifications Text = CompletionText }; - protected void Completed() - { - CompletionTarget?.Invoke(CreateCompletionNotification()); - base.Close(); - } - public override bool DisplayOnTop => false; public override bool IsImportant => false; From 229e1a8ef7a97639dd328c90ca96afaf6f5ee424 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Sep 2022 18:42:37 +0900 Subject: [PATCH 504/532] Fix notification overlay being present when it doesn't need to --- .../UserInterface/TestSceneNotificationOverlay.cs | 14 ++++++++++++++ osu.Game/Overlays/NotificationOverlayToastTray.cs | 2 ++ 2 files changed, 16 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 288f1f78bc..8c7ad5f061 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; @@ -44,6 +45,18 @@ namespace osu.Game.Tests.Visual.UserInterface notificationOverlay.UnreadCount.ValueChanged += count => { displayedCount.Text = $"displayed count: {count.NewValue}"; }; }); + [Test] + public void TestPresence() + { + AddAssert("tray not present", () => !notificationOverlay.ChildrenOfType().Single().IsPresent); + AddAssert("overlay not present", () => !notificationOverlay.IsPresent); + + AddStep(@"post notification", sendBackgroundNotification); + + AddUntilStep("wait tray not present", () => !notificationOverlay.ChildrenOfType().Single().IsPresent); + AddUntilStep("wait overlay not present", () => !notificationOverlay.IsPresent); + } + [Test] public void TestCompleteProgress() { @@ -63,6 +76,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("wait completion", () => notification.State == ProgressNotificationState.Completed); AddAssert("Completion toast shown", () => notificationOverlay.ToastCount == 1); + AddUntilStep("wait forwarded", () => notificationOverlay.ToastCount == 0); } [Test] diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index ebf8fb32a9..df9bbac887 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -24,6 +24,8 @@ namespace osu.Game.Overlays /// public class NotificationOverlayToastTray : CompositeDrawable { + public override bool IsPresent => IsDisplayingToasts; + public bool IsDisplayingToasts => displayedCount > 0; private FillFlowContainer toastFlow = null!; From 0514c961915a250c22a32bf3d466ee5d8239b072 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Sep 2022 19:20:01 +0900 Subject: [PATCH 505/532] Fix incorrect count tracking when notification is manually disposed --- .../UserInterface/TestSceneNotificationOverlay.cs | 13 +++++++++++++ osu.Game/Overlays/NotificationOverlayToastTray.cs | 13 ++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 8c7ad5f061..699b8f7d89 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -57,6 +57,19 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("wait overlay not present", () => !notificationOverlay.IsPresent); } + [Test] + public void TestPresenceWithManualDismiss() + { + AddAssert("tray not present", () => !notificationOverlay.ChildrenOfType().Single().IsPresent); + AddAssert("overlay not present", () => !notificationOverlay.IsPresent); + + AddStep(@"post notification", sendBackgroundNotification); + AddStep("click notification", () => notificationOverlay.ChildrenOfType().Single().TriggerClick()); + + AddUntilStep("wait tray not present", () => !notificationOverlay.ChildrenOfType().Single().IsPresent); + AddUntilStep("wait overlay not present", () => !notificationOverlay.IsPresent); + } + [Test] public void TestCompleteProgress() { diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index df9bbac887..4f98b9f40c 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -97,6 +97,8 @@ namespace osu.Game.Overlays public void Post(Notification notification) { + notification.Closed += stopTrackingNotification; + ++runningDepth; displayedCount++; @@ -139,14 +141,23 @@ namespace osu.Game.Overlays notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint); notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ => { + notification.Closed -= stopTrackingNotification; + if (!notification.WasClosed) + stopTrackingNotification(); + RemoveInternal(notification, false); - displayedCount--; ForwardNotificationToPermanentStore?.Invoke(notification); notification.FadeIn(300, Easing.OutQuint); }); } + private void stopTrackingNotification() + { + Debug.Assert(displayedCount > 0); + displayedCount--; + } + protected override void Update() { base.Update(); From 510972e3add24d7aebe619d68a56017a70fcbf67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Sep 2022 19:35:15 +0900 Subject: [PATCH 506/532] Avoid reference counting by using height calculation for `IsPresent` instead The reference counting was to guarantee performance (zero allocations) when the notification overlay was not required, but adds extra complexity. Instead, the toast tray now uses its ongoing height calculation as a metric for presence. --- .../Overlays/NotificationOverlayToastTray.cs | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index 4f98b9f40c..f9c4f657eb 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -24,9 +24,9 @@ namespace osu.Game.Overlays /// public class NotificationOverlayToastTray : CompositeDrawable { - public override bool IsPresent => IsDisplayingToasts; + public override bool IsPresent => toastContentBackground.Height > 0 || toastFlow.Count > 0; - public bool IsDisplayingToasts => displayedCount > 0; + public bool IsDisplayingToasts => allNotifications.Any(); private FillFlowContainer toastFlow = null!; private BufferedContainer toastContentBackground = null!; @@ -42,8 +42,6 @@ namespace osu.Game.Overlays private int runningDepth; - private int displayedCount; - [BackgroundDependencyLoader] private void load() { @@ -61,6 +59,7 @@ namespace osu.Game.Overlays colourProvider.Background6.Opacity(0.7f), colourProvider.Background6.Opacity(0.5f)), RelativeSizeAxes = Axes.Both, + Height = 0, }.WithEffect(new BlurEffect { PadExtent = true, @@ -97,10 +96,7 @@ namespace osu.Game.Overlays public void Post(Notification notification) { - notification.Closed += stopTrackingNotification; - ++runningDepth; - displayedCount++; int depth = notification.DisplayOnTop ? -runningDepth : runningDepth; @@ -141,10 +137,6 @@ namespace osu.Game.Overlays notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint); notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ => { - notification.Closed -= stopTrackingNotification; - if (!notification.WasClosed) - stopTrackingNotification(); - RemoveInternal(notification, false); ForwardNotificationToPermanentStore?.Invoke(notification); @@ -152,17 +144,11 @@ namespace osu.Game.Overlays }); } - private void stopTrackingNotification() - { - Debug.Assert(displayedCount > 0); - displayedCount--; - } - protected override void Update() { base.Update(); - float height = toastFlow.DrawHeight + 120; + float height = toastFlow.Count > 0 ? toastFlow.DrawHeight + 120 : 0; float alpha = toastFlow.Count > 0 ? MathHelper.Clamp(toastFlow.DrawHeight / 41, 0, 1) * toastFlow.Children.Max(n => n.Alpha) : 0; toastContentBackground.Height = (float)Interpolation.DampContinuously(toastContentBackground.Height, height, 10, Clock.ElapsedFrameTime); From 9e3228aa651181b5a531ccf8fc6b5da15962d7ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Sep 2022 04:07:49 +0900 Subject: [PATCH 507/532] Fix completion notification not being posted if completion occurs during `NotificationOverlay` load --- .../Navigation/TestSceneStartupImport.cs | 4 +-- .../ProgressCompletionNotification.cs | 2 -- .../Notifications/ProgressNotification.cs | 33 ++++++++++++++----- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs index cd53bf3af5..552eb82419 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -13,7 +11,7 @@ namespace osu.Game.Tests.Visual.Navigation { public class TestSceneStartupImport : OsuGameTestScene { - private string importFilename; + private string? importFilename; protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { importFilename }); diff --git a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs index 49d558285c..3cbdf7edf7 100644 --- a/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressCompletionNotification.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Framework.Graphics.Colour; diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index b5af3a56a1..64ad69adf3 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Notifications base.LoadComplete(); // we may have received changes before we were displayed. - updateState(); + Scheduler.AddOnce(updateState); } private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); @@ -87,13 +87,8 @@ namespace osu.Game.Overlays.Notifications state = value; - if (IsLoaded) - { - Schedule(updateState); - - if (state == ProgressNotificationState.Completed) - CompletionTarget?.Invoke(CreateCompletionNotification()); - } + Scheduler.AddOnce(updateState); + attemptPostCompletion(); } } @@ -146,11 +141,33 @@ namespace osu.Game.Overlays.Notifications case ProgressNotificationState.Completed: loadingSpinner.Hide(); + attemptPostCompletion(); base.Close(); break; } } + private bool completionSent; + + /// + /// Attempt to post a completion notification. + /// + private void attemptPostCompletion() + { + if (state != ProgressNotificationState.Completed) return; + + // This notification may not have been posted yet (and thus may not have a target to post the completion to). + // Completion posting will be re-attempted in a scheduled invocation. + if (CompletionTarget == null) + return; + + if (completionSent) + return; + + CompletionTarget.Invoke(CreateCompletionNotification()); + completionSent = true; + } + private ProgressNotificationState state; protected virtual Notification CreateCompletionNotification() => new ProgressCompletionNotification From abf02426862da854f93a257b9d2cd520f601ab4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Sep 2022 16:16:54 +0900 Subject: [PATCH 508/532] Add failing test for incorrect start time of storyboard elements --- .../Formats/LegacyStoryboardDecoderTest.cs | 17 +++++++++++++++++ .../loop-containing-earlier-non-zero-fade.osb | 8 ++++++++ 2 files changed, 25 insertions(+) create mode 100644 osu.Game.Tests/Resources/loop-containing-earlier-non-zero-fade.osb diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 57dded8ee4..8dcbdb8435 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -117,6 +117,23 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestEarliestStartTimeWithLoopAlphas() + { + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("loop-containing-earlier-non-zero-fade.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); + Assert.AreEqual(1, background.Elements.Count); + Assert.AreEqual(1000, background.Elements[0].StartTime); + Assert.AreEqual(1000, storyboard.EarliestEventTime); + } + } + [Test] public void TestDecodeVariableWithSuffix() { diff --git a/osu.Game.Tests/Resources/loop-containing-earlier-non-zero-fade.osb b/osu.Game.Tests/Resources/loop-containing-earlier-non-zero-fade.osb new file mode 100644 index 0000000000..5c80bc620c --- /dev/null +++ b/osu.Game.Tests/Resources/loop-containing-earlier-non-zero-fade.osb @@ -0,0 +1,8 @@ +osu file format v14 + +[Events] +//Storyboard Layer 0 (Background) +Sprite,Background,TopCentre,"img.jpg",320,240 + L,1000,1 + F,0,0,,1 // fade inside a loop with non-zero alpha and an earlier start time should be the true start time.. + F,0,2000,,0 // ..not a zero alpha fade with a later start time From a5e57b083c09a136a0248c07266b9838bd147bf7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Sep 2022 15:48:54 +0900 Subject: [PATCH 509/532] Remove `EarliestDisplayTime`'s input to `CommandStartTime` --- osu.Game/Storyboards/CommandTimelineGroup.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index 6fc9f60177..de5da3118a 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -47,30 +47,11 @@ namespace osu.Game.Storyboards }; } - /// - /// Returns the earliest visible time. Will be null unless this group's first command has a start value of zero. - /// - public double? EarliestDisplayedTime - { - get - { - var first = Alpha.Commands.FirstOrDefault(); - - return first?.StartValue == 0 ? first.StartTime : null; - } - } - [JsonIgnore] public double CommandsStartTime { get { - // if the first alpha command starts at zero it should be given priority over anything else. - // this is due to it creating a state where the target is not present before that time, causing any other events to not be visible. - double? earliestDisplay = EarliestDisplayedTime; - if (earliestDisplay != null) - return earliestDisplay.Value; - double min = double.MaxValue; for (int i = 0; i < timelines.Length; i++) From bea42d286264e4d866dae8b9d58e0771b519f222 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Sep 2022 16:09:16 +0900 Subject: [PATCH 510/532] Handle earliest-alpha-start-time logic in `StoryboardSprite` itself --- osu.Game/Storyboards/StoryboardSprite.cs | 50 ++++++++++++++++++------ 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index a759482b5d..249be39b65 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -30,24 +30,52 @@ namespace osu.Game.Storyboards { get { - // check for presence affecting commands as an initial pass. - double earliestStartTime = TimelineGroup.EarliestDisplayedTime ?? double.MaxValue; + // To get the initial start time, we need to check whether the first alpha command to exist has an initial value of zero. + // A start value of zero on an alpha governs, above all else, the first valid display time of a sprite. + // + // To make this valid, we need to aggregate all alpha commands across all loops before checking for the first, time-wise, else we could run into a scenario where: + // L,1000,1 + // F,0,0,,1 # this NEEDS TO BE considered the StartTime... but has a StartValue greater than zero so won't in the initial pass. + // F,0,2000,,0 # ..this will be treated as earliest start due to a StartValue of zero. - foreach (var l in loops) + double zeroAlphaStartTimeOverride = double.MaxValue; + + var topLevelFirstAlphaCommand = TimelineGroup.Alpha.Commands.FirstOrDefault(); + if (topLevelFirstAlphaCommand?.StartValue == 0) + zeroAlphaStartTimeOverride = topLevelFirstAlphaCommand.StartTime; + + foreach (var loop in loops) { - if (l.EarliestDisplayedTime is double loopEarliestDisplayTime) - earliestStartTime = Math.Min(earliestStartTime, l.LoopStartTime + loopEarliestDisplayTime); + var loopFirstAlphaCommand = loop.Alpha.Commands.FirstOrDefault(); + + if (loopFirstAlphaCommand == null) + continue; + + double loopFirstAlphaStartTime = loopFirstAlphaCommand.StartTime + loop.LoopStartTime; + + if (loopFirstAlphaCommand.StartValue == 0) + { + // Found a loop containing a zero StartValue earlier than previous. + zeroAlphaStartTimeOverride = loopFirstAlphaStartTime; + } + else if (loopFirstAlphaStartTime < zeroAlphaStartTimeOverride) + { + // If a loop's first alpha command's StartValue is non-zero, we need to check whether the command's StartTime is less than any previously found zero alpha command. + // If this is the case, we want to abort zero alpha override logic and use the earliest command time instead. + // This is because if the first alpha command is non-zero, the sprite will be shown at that alpha from the time of the first command (of any type, not just alpha). + zeroAlphaStartTimeOverride = double.MaxValue; + break; + } } - if (earliestStartTime < double.MaxValue) - return earliestStartTime; - - // if an alpha-affecting command was not found, use the earliest of any command. - earliestStartTime = TimelineGroup.StartTime; + if (zeroAlphaStartTimeOverride < double.MaxValue) + return zeroAlphaStartTimeOverride; + // if we got to this point, either no alpha commands were present, or the earliest had a non-zero start value. + // the sprite's StartTime will be determined by the earliest command, of any type. + double earliestStartTime = TimelineGroup.StartTime; foreach (var l in loops) earliestStartTime = Math.Min(earliestStartTime, l.StartTime); - return earliestStartTime; } } From fa0a4614f8d90f18604349aef659ee521ad72ef0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Sep 2022 16:40:59 +0900 Subject: [PATCH 511/532] Add failing test for second incorrect case of start time handling --- .../Beatmaps/Formats/LegacyStoryboardDecoderTest.cs | 5 ++++- .../Resources/loop-containing-earlier-non-zero-fade.osb | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 8dcbdb8435..6e41043b0b 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -128,8 +128,11 @@ namespace osu.Game.Tests.Beatmaps.Formats var storyboard = decoder.Decode(stream); StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); - Assert.AreEqual(1, background.Elements.Count); + Assert.AreEqual(2, background.Elements.Count); + Assert.AreEqual(1000, background.Elements[0].StartTime); + Assert.AreEqual(1000, background.Elements[1].StartTime); + Assert.AreEqual(1000, storyboard.EarliestEventTime); } } diff --git a/osu.Game.Tests/Resources/loop-containing-earlier-non-zero-fade.osb b/osu.Game.Tests/Resources/loop-containing-earlier-non-zero-fade.osb index 5c80bc620c..2ff7f9f56f 100644 --- a/osu.Game.Tests/Resources/loop-containing-earlier-non-zero-fade.osb +++ b/osu.Game.Tests/Resources/loop-containing-earlier-non-zero-fade.osb @@ -6,3 +6,9 @@ Sprite,Background,TopCentre,"img.jpg",320,240 L,1000,1 F,0,0,,1 // fade inside a loop with non-zero alpha and an earlier start time should be the true start time.. F,0,2000,,0 // ..not a zero alpha fade with a later start time + +Sprite,Background,TopCentre,"img.jpg",320,240 + L,2000,1 + F,0,0,24,0 // fade inside a loop with zero alpha but later start time than the top-level zero alpha start time. + F,0,24,48,1 + F,0,1000,,1 // ..so this should be the true start time From d667f4683050c7e2bcebf6488f887c2ff421462a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Sep 2022 16:41:32 +0900 Subject: [PATCH 512/532] Refactor alpha check to not overwrite sourced overrides with values from later commands --- osu.Game/Storyboards/StoryboardSprite.cs | 45 ++++++++++++++---------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 249be39b65..0764969d02 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -32,17 +32,18 @@ namespace osu.Game.Storyboards { // To get the initial start time, we need to check whether the first alpha command to exist has an initial value of zero. // A start value of zero on an alpha governs, above all else, the first valid display time of a sprite. - // - // To make this valid, we need to aggregate all alpha commands across all loops before checking for the first, time-wise, else we could run into a scenario where: - // L,1000,1 - // F,0,0,,1 # this NEEDS TO BE considered the StartTime... but has a StartValue greater than zero so won't in the initial pass. - // F,0,2000,,0 # ..this will be treated as earliest start due to a StartValue of zero. - double zeroAlphaStartTimeOverride = double.MaxValue; + double earliestAlphaTime = double.MaxValue; var topLevelFirstAlphaCommand = TimelineGroup.Alpha.Commands.FirstOrDefault(); - if (topLevelFirstAlphaCommand?.StartValue == 0) - zeroAlphaStartTimeOverride = topLevelFirstAlphaCommand.StartTime; + + if (topLevelFirstAlphaCommand != null) + { + earliestAlphaTime = topLevelFirstAlphaCommand.StartTime; + if (topLevelFirstAlphaCommand.StartValue == 0) + // The found alpha command has a zero StartValue, so should be used as the new override. + zeroAlphaStartTimeOverride = topLevelFirstAlphaCommand.StartTime; + } foreach (var loop in loops) { @@ -53,18 +54,24 @@ namespace osu.Game.Storyboards double loopFirstAlphaStartTime = loopFirstAlphaCommand.StartTime + loop.LoopStartTime; - if (loopFirstAlphaCommand.StartValue == 0) + // Found an alpha command with an earlier start time than an override. + if (loopFirstAlphaStartTime < earliestAlphaTime) { - // Found a loop containing a zero StartValue earlier than previous. - zeroAlphaStartTimeOverride = loopFirstAlphaStartTime; - } - else if (loopFirstAlphaStartTime < zeroAlphaStartTimeOverride) - { - // If a loop's first alpha command's StartValue is non-zero, we need to check whether the command's StartTime is less than any previously found zero alpha command. - // If this is the case, we want to abort zero alpha override logic and use the earliest command time instead. - // This is because if the first alpha command is non-zero, the sprite will be shown at that alpha from the time of the first command (of any type, not just alpha). - zeroAlphaStartTimeOverride = double.MaxValue; - break; + earliestAlphaTime = loopFirstAlphaStartTime; + + if (loopFirstAlphaCommand.StartValue == 0) + { + // The found alpha command has a zero StartValue, so should be used as the new override. + zeroAlphaStartTimeOverride = loopFirstAlphaStartTime; + } + else if (loopFirstAlphaStartTime < zeroAlphaStartTimeOverride) + { + // The found alpha command's StartValue is non-zero. + // In this case, we want to reset any found alpha override (so if this is the new earliest alpha, + // we will fall through to the earlier command time logic below). + zeroAlphaStartTimeOverride = double.MaxValue; + break; + } } } From 677708c5e4b0bad1b112ed6402cfa9be05d5ab73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Sep 2022 16:58:51 +0900 Subject: [PATCH 513/532] Rewrite logic using a list --- osu.Game/Storyboards/StoryboardSprite.cs | 62 ++++++++---------------- 1 file changed, 19 insertions(+), 43 deletions(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 0764969d02..f77b3a812e 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -30,56 +30,32 @@ namespace osu.Game.Storyboards { get { - // To get the initial start time, we need to check whether the first alpha command to exist has an initial value of zero. - // A start value of zero on an alpha governs, above all else, the first valid display time of a sprite. - double zeroAlphaStartTimeOverride = double.MaxValue; - double earliestAlphaTime = double.MaxValue; + // To get the initial start time, we need to check whether the first alpha command to exist (across all loops) has a StartValue of zero. + // A StartValue of zero governs, above all else, the first valid display time of a sprite. + // + // You can imagine that the first command of each type decides that type's start value, so if the initial alpha is zero, + // anything before that point can be ignored (the sprite is not visible after all). + var alphaCommands = new List<(double startTime, bool isZeroStartValue)>(); - var topLevelFirstAlphaCommand = TimelineGroup.Alpha.Commands.FirstOrDefault(); - - if (topLevelFirstAlphaCommand != null) - { - earliestAlphaTime = topLevelFirstAlphaCommand.StartTime; - if (topLevelFirstAlphaCommand.StartValue == 0) - // The found alpha command has a zero StartValue, so should be used as the new override. - zeroAlphaStartTimeOverride = topLevelFirstAlphaCommand.StartTime; - } + var command = TimelineGroup.Alpha.Commands.FirstOrDefault(); + if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0)); foreach (var loop in loops) { - var loopFirstAlphaCommand = loop.Alpha.Commands.FirstOrDefault(); - - if (loopFirstAlphaCommand == null) - continue; - - double loopFirstAlphaStartTime = loopFirstAlphaCommand.StartTime + loop.LoopStartTime; - - // Found an alpha command with an earlier start time than an override. - if (loopFirstAlphaStartTime < earliestAlphaTime) - { - earliestAlphaTime = loopFirstAlphaStartTime; - - if (loopFirstAlphaCommand.StartValue == 0) - { - // The found alpha command has a zero StartValue, so should be used as the new override. - zeroAlphaStartTimeOverride = loopFirstAlphaStartTime; - } - else if (loopFirstAlphaStartTime < zeroAlphaStartTimeOverride) - { - // The found alpha command's StartValue is non-zero. - // In this case, we want to reset any found alpha override (so if this is the new earliest alpha, - // we will fall through to the earlier command time logic below). - zeroAlphaStartTimeOverride = double.MaxValue; - break; - } - } + command = loop.Alpha.Commands.FirstOrDefault(); + if (command != null) alphaCommands.Add((command.StartTime + loop.StartTime, command.StartValue == 0)); } - if (zeroAlphaStartTimeOverride < double.MaxValue) - return zeroAlphaStartTimeOverride; + if (alphaCommands.Count > 0) + { + var firstAlpha = alphaCommands.OrderBy(t => t.startTime).First(); - // if we got to this point, either no alpha commands were present, or the earliest had a non-zero start value. - // the sprite's StartTime will be determined by the earliest command, of any type. + if (firstAlpha.isZeroStartValue) + return firstAlpha.startTime; + } + + // If we got to this point, either no alpha commands were present, or the earliest had a non-zero start value. + // The sprite's StartTime will be determined by the earliest command, regardless of type. double earliestStartTime = TimelineGroup.StartTime; foreach (var l in loops) earliestStartTime = Math.Min(earliestStartTime, l.StartTime); From 6e52dbb2668768eb8654251a8a4bd3d13190443a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Sep 2022 17:30:24 +0900 Subject: [PATCH 514/532] Update `IsDisplayingToasts` to check the flow count directly --- osu.Game/Overlays/NotificationOverlayToastTray.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index f9c4f657eb..40324963fc 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays { public override bool IsPresent => toastContentBackground.Height > 0 || toastFlow.Count > 0; - public bool IsDisplayingToasts => allNotifications.Any(); + public bool IsDisplayingToasts => toastFlow.Count > 0; private FillFlowContainer toastFlow = null!; private BufferedContainer toastContentBackground = null!; @@ -36,9 +36,12 @@ namespace osu.Game.Overlays public Action? ForwardNotificationToPermanentStore { get; set; } - public int UnreadCount => allNotifications.Count(n => !n.WasClosed && !n.Read); + public int UnreadCount => allDisplayedNotifications.Count(n => !n.WasClosed && !n.Read); - private IEnumerable allNotifications => toastFlow.Concat(InternalChildren.OfType()); + /// + /// Notifications contained in the toast flow, or in a detached state while they animate during forwarding to the main overlay. + /// + private IEnumerable allDisplayedNotifications => toastFlow.Concat(InternalChildren.OfType()); private int runningDepth; From 9f2ea54e40c993a9b5091228f455a88bbb33d4fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Sep 2022 17:45:53 +0900 Subject: [PATCH 515/532] Tidy up `TestSceneLeadIn` constant for loop offset to read better --- osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index c066f1417c..c18a78fe3c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -66,18 +66,20 @@ namespace osu.Game.Tests.Visual.Gameplay [TestCase(-10000, -10000, true)] public void TestStoryboardProducesCorrectStartTimeFadeInAfterOtherEvents(double firstStoryboardEvent, double expectedStartTime, bool addEventToLoop) { + const double loop_start_time = -20000; + var storyboard = new Storyboard(); var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); // these should be ignored as we have an alpha visibility blocker proceeding this command. - sprite.TimelineGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1); - var loopGroup = sprite.AddLoop(-20000, 50); - loopGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1); + sprite.TimelineGroup.Scale.Add(Easing.None, loop_start_time, -18000, 0, 1); + var loopGroup = sprite.AddLoop(loop_start_time, 50); + loopGroup.Scale.Add(Easing.None, loop_start_time, -18000, 0, 1); var target = addEventToLoop ? loopGroup : sprite.TimelineGroup; - double targetTime = addEventToLoop ? 20000 : 0; - target.Alpha.Add(Easing.None, targetTime + firstStoryboardEvent, targetTime + firstStoryboardEvent + 500, 0, 1); + double loopRelativeOffset = addEventToLoop ? -loop_start_time : 0; + target.Alpha.Add(Easing.None, loopRelativeOffset + firstStoryboardEvent, loopRelativeOffset + firstStoryboardEvent + 500, 0, 1); // these should be ignored due to being in the future. sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1); From a3de5f808e39885505e061bdcab0716f226d8e22 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Sep 2022 17:46:03 +0900 Subject: [PATCH 516/532] Fix typo in `LoopStartTime` addition --- osu.Game/Storyboards/StoryboardSprite.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index f77b3a812e..1eeaa0f084 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -43,7 +43,7 @@ namespace osu.Game.Storyboards foreach (var loop in loops) { command = loop.Alpha.Commands.FirstOrDefault(); - if (command != null) alphaCommands.Add((command.StartTime + loop.StartTime, command.StartValue == 0)); + if (command != null) alphaCommands.Add((command.StartTime + loop.LoopStartTime, command.StartValue == 0)); } if (alphaCommands.Count > 0) From 579e7e1f174a6db56827b80bbfffb1baaf7cd727 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Sep 2022 18:08:51 +0900 Subject: [PATCH 517/532] Fix deleting a difficulty not updating the beatmap set hash --- .../Visual/Editing/TestSceneDifficultyDelete.cs | 3 +++ osu.Game/Beatmaps/BeatmapManager.cs | 11 +++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs index 1520f64ec0..4366b1c0bb 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultyDelete.cs @@ -45,6 +45,7 @@ namespace osu.Game.Tests.Visual.Editing { Guid deletedDifficultyID = Guid.Empty; int countBeforeDeletion = 0; + string beatmapSetHashBefore = string.Empty; for (int i = 0; i < 12; i++) { @@ -55,6 +56,7 @@ namespace osu.Game.Tests.Visual.Editing { deletedDifficultyID = EditorBeatmap.BeatmapInfo.ID; countBeforeDeletion = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count; + beatmapSetHashBefore = Beatmap.Value.BeatmapSetInfo.Hash; }); AddStep("click File", () => this.ChildrenOfType().First().TriggerClick()); @@ -72,6 +74,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert($"difficulty {i} is deleted", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Select(b => b.ID), () => Does.Not.Contain(deletedDifficultyID)); AddAssert("count decreased by one", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, () => Is.EqualTo(countBeforeDeletion - 1)); + AddAssert("set hash changed", () => Beatmap.Value.BeatmapSetInfo.Hash, () => Is.Not.EqualTo(beatmapSetHashBefore)); } } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d784f1e627..d55ccb4b41 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -319,8 +319,7 @@ namespace osu.Game.Beatmaps AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo)); - setInfo.Hash = beatmapImporter.ComputeHash(setInfo); - setInfo.Status = BeatmapOnlineStatus.LocallyModified; + updateHashAndMarkDirty(setInfo); Realm.Write(r => { @@ -384,6 +383,8 @@ namespace osu.Game.Beatmaps DeleteFile(setInfo, beatmapInfo.File); setInfo.Beatmaps.Remove(beatmapInfo); + + updateHashAndMarkDirty(setInfo); }); } @@ -440,6 +441,12 @@ namespace osu.Game.Beatmaps public Task?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) => beatmapImporter.ImportAsUpdate(notification, importTask, original); + private void updateHashAndMarkDirty(BeatmapSetInfo setInfo) + { + setInfo.Hash = beatmapImporter.ComputeHash(setInfo); + setInfo.Status = BeatmapOnlineStatus.LocallyModified; + } + #region Implementation of ICanAcceptFiles public Task Import(params string[] paths) => beatmapImporter.Import(paths); From db15bd56e87630d0a6bce7e3f31422b82e81b933 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Sep 2022 18:10:59 +0900 Subject: [PATCH 518/532] Invalidate working beatmap cache when calling `DeleteDifficultyImmediately` rather than in editor code --- osu.Game/Beatmaps/BeatmapManager.cs | 1 + osu.Game/Screens/Edit/Editor.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d55ccb4b41..2c6edb64f8 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -385,6 +385,7 @@ namespace osu.Game.Beatmaps setInfo.Beatmaps.Remove(beatmapInfo); updateHashAndMarkDirty(setInfo); + workingBeatmapCache.Invalidate(setInfo); }); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 6e06683e38..3dfc7010f3 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -929,7 +929,7 @@ namespace osu.Game.Screens.Edit // of note, we're still working with the cloned version, so indices are all prior to deletion. BeatmapInfo nextToShow = difficultiesBeforeDeletion[deletedIndex == 0 ? 1 : deletedIndex - 1]; - Beatmap.Value = beatmapManager.GetWorkingBeatmap(nextToShow, true); + Beatmap.Value = beatmapManager.GetWorkingBeatmap(nextToShow); SwitchToDifficulty(nextToShow); } From a2f8ff825e88bc02c3a2e257d5d0f78a1af71a13 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Sep 2022 17:27:29 +0900 Subject: [PATCH 519/532] Also ignore drum roll strong judgement --- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 2 ++ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 3dab8d2e13..3325eda7cf 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -96,6 +96,8 @@ namespace osu.Game.Rulesets.Taiko.Objects public class StrongNestedHit : StrongNestedHitObject { + // The strong hit of the drum roll doesn't actually provide any score. + public override Judgement CreateJudgement() => new IgnoreJudgement(); } #region LegacyBeatmapEncoder diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index ccca5587b7..cc71ba5401 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -317,6 +317,9 @@ namespace osu.Game.Rulesets.Taiko.UI break; default: + if (!result.Type.IsScorable()) + break; + judgementContainer.Add(judgementPools[result.Type].Get(j => { j.Apply(result, judgedObject); From 2ca63b50309b8d839dffaced3bb9d59ab51766fc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 6 Sep 2022 18:02:17 +0900 Subject: [PATCH 520/532] Add tests for all taiko judgements --- .../Judgements/JudgementTest.cs | 96 +++++++++ .../Judgements/TestSceneDrumRollJudgements.cs | 201 ++++++++++++++++++ .../Judgements/TestSceneHitJudgements.cs | 133 ++++++++++++ .../Judgements/TestSceneSwellJudgements.cs | 118 ++++++++++ .../TestSceneSwellJudgements.cs | 42 ---- 5 files changed, 548 insertions(+), 42 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs delete mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs new file mode 100644 index 0000000000..c9e8fefead --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class JudgementTest : RateAdjustedBeatmapTestScene + { + private ScoreAccessibleReplayPlayer currentPlayer = null!; + protected List JudgementResults { get; private set; } = null!; + + protected void AssertJudgementCount(int count) + { + AddAssert($"{count} judgement{(count > 0 ? "s" : "")}", () => JudgementResults, () => Has.Count.EqualTo(count)); + } + + protected void AssertResult(int index, HitResult expectedResult) + { + AddAssert($"{typeof(T).ReadableName()} ({index}) judged as {expectedResult}", + () => JudgementResults.Where(j => j.HitObject is T).OrderBy(j => j.HitObject.StartTime).ElementAt(index).Type, + () => Is.EqualTo(expectedResult)); + } + + protected void PerformTest(List frames, Beatmap? beatmap = null) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(beatmap); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) JudgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + JudgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + protected Beatmap CreateBeatmap(params TaikoHitObject[] hitObjects) + { + var beatmap = new Beatmap + { + HitObjects = hitObjects.ToList(), + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new TaikoRuleset().RulesetInfo + }, + }; + + beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); + return beatmap; + } + + private class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs new file mode 100644 index 0000000000..2c28c3dad5 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs @@ -0,0 +1,201 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; + +namespace osu.Game.Rulesets.Taiko.Tests.Judgements +{ + public class TestSceneDrumRollJudgements : JudgementTest + { + [Test] + public void TestHitAllDrumRoll() + { + const double hit_time = 1000; + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(1000, TaikoAction.LeftCentre), + new TaikoReplayFrame(1001), + new TaikoReplayFrame(2000, TaikoAction.LeftCentre), + new TaikoReplayFrame(2001), + }, CreateBeatmap(new DrumRoll + { + StartTime = hit_time, + Duration = 1000 + })); + + AssertJudgementCount(3); + AssertResult(0, HitResult.SmallBonus); + AssertResult(1, HitResult.SmallBonus); + AssertResult(0, HitResult.IgnoreHit); + } + + [Test] + public void TestHitSomeDrumRoll() + { + const double hit_time = 1000; + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(2000, TaikoAction.LeftCentre), + new TaikoReplayFrame(2001), + }, CreateBeatmap(new DrumRoll + { + StartTime = hit_time, + Duration = 1000 + })); + + AssertJudgementCount(3); + AssertResult(0, HitResult.IgnoreMiss); + AssertResult(1, HitResult.SmallBonus); + AssertResult(0, HitResult.IgnoreHit); + } + + [Test] + public void TestHitNoneDrumRoll() + { + const double hit_time = 1000; + + PerformTest(new List + { + new TaikoReplayFrame(0), + }, CreateBeatmap(new DrumRoll + { + StartTime = hit_time, + Duration = 1000 + })); + + AssertJudgementCount(3); + AssertResult(0, HitResult.IgnoreMiss); + AssertResult(1, HitResult.IgnoreMiss); + AssertResult(0, HitResult.IgnoreHit); + } + + [Test] + public void TestHitAllStrongDrumRollWithOneKey() + { + const double hit_time = 1000; + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(1000, TaikoAction.LeftCentre), + new TaikoReplayFrame(1001), + new TaikoReplayFrame(2000, TaikoAction.LeftCentre), + new TaikoReplayFrame(2001), + }, CreateBeatmap(new DrumRoll + { + StartTime = hit_time, + Duration = 1000, + IsStrong = true + })); + + AssertJudgementCount(6); + + AssertResult(0, HitResult.SmallBonus); + AssertResult(0, HitResult.LargeBonus); + + AssertResult(1, HitResult.SmallBonus); + AssertResult(1, HitResult.LargeBonus); + + AssertResult(0, HitResult.IgnoreHit); + AssertResult(2, HitResult.IgnoreHit); + } + + [Test] + public void TestHitSomeStrongDrumRollWithOneKey() + { + const double hit_time = 1000; + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(2000, TaikoAction.LeftCentre), + new TaikoReplayFrame(2001), + }, CreateBeatmap(new DrumRoll + { + StartTime = hit_time, + Duration = 1000, + IsStrong = true + })); + + AssertJudgementCount(6); + + AssertResult(0, HitResult.IgnoreMiss); + AssertResult(0, HitResult.IgnoreMiss); + + AssertResult(1, HitResult.SmallBonus); + AssertResult(1, HitResult.LargeBonus); + + AssertResult(0, HitResult.IgnoreHit); + AssertResult(2, HitResult.IgnoreHit); + } + + [Test] + public void TestHitAllStrongDrumRollWithBothKeys() + { + const double hit_time = 1000; + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre), + new TaikoReplayFrame(1001), + new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre), + new TaikoReplayFrame(2001), + }, CreateBeatmap(new DrumRoll + { + StartTime = hit_time, + Duration = 1000, + IsStrong = true + })); + + AssertJudgementCount(6); + + AssertResult(0, HitResult.SmallBonus); + AssertResult(0, HitResult.LargeBonus); + + AssertResult(1, HitResult.SmallBonus); + AssertResult(1, HitResult.LargeBonus); + + AssertResult(0, HitResult.IgnoreHit); + AssertResult(2, HitResult.IgnoreHit); + } + + [Test] + public void TestHitSomeStrongDrumRollWithBothKeys() + { + const double hit_time = 1000; + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre), + new TaikoReplayFrame(2001), + }, CreateBeatmap(new DrumRoll + { + StartTime = hit_time, + Duration = 1000, + IsStrong = true + })); + + AssertJudgementCount(6); + + AssertResult(0, HitResult.IgnoreMiss); + AssertResult(0, HitResult.IgnoreMiss); + + AssertResult(1, HitResult.SmallBonus); + AssertResult(1, HitResult.LargeBonus); + + AssertResult(0, HitResult.IgnoreHit); + AssertResult(2, HitResult.IgnoreHit); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs new file mode 100644 index 0000000000..a405f0e8ba --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs @@ -0,0 +1,133 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; + +namespace osu.Game.Rulesets.Taiko.Tests.Judgements +{ + public class TestSceneHitJudgements : JudgementTest + { + [Test] + public void TestHitCentreHit() + { + const double hit_time = 1000; + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time, TaikoAction.LeftCentre), + }, CreateBeatmap(new Hit + { + Type = HitType.Centre, + StartTime = hit_time + })); + + AssertJudgementCount(1); + AssertResult(0, HitResult.Great); + } + + [Test] + public void TestHitRimHit() + { + const double hit_time = 1000; + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time, TaikoAction.LeftRim), + }, CreateBeatmap(new Hit + { + Type = HitType.Rim, + StartTime = hit_time + })); + + AssertJudgementCount(1); + AssertResult(0, HitResult.Great); + } + + [Test] + public void TestMissHit() + { + const double hit_time = 1000; + + PerformTest(new List + { + new TaikoReplayFrame(0) + }, CreateBeatmap(new Hit + { + Type = HitType.Centre, + StartTime = hit_time + })); + + AssertJudgementCount(1); + AssertResult(0, HitResult.Miss); + } + + [Test] + public void TestHitStrongHitWithOneKey() + { + const double hit_time = 1000; + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time, TaikoAction.LeftCentre), + }, CreateBeatmap(new Hit + { + Type = HitType.Centre, + StartTime = hit_time, + IsStrong = true + })); + + AssertJudgementCount(2); + AssertResult(0, HitResult.Great); + AssertResult(0, HitResult.IgnoreMiss); + } + + [Test] + public void TestHitStrongHitWithBothKeys() + { + const double hit_time = 1000; + + PerformTest(new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time, TaikoAction.LeftCentre, TaikoAction.RightCentre), + }, CreateBeatmap(new Hit + { + Type = HitType.Centre, + StartTime = hit_time, + IsStrong = true + })); + + AssertJudgementCount(2); + AssertResult(0, HitResult.Great); + AssertResult(0, HitResult.LargeBonus); + } + + [Test] + public void TestMissStrongHit() + { + const double hit_time = 1000; + + PerformTest(new List + { + new TaikoReplayFrame(0), + }, CreateBeatmap(new Hit + { + Type = HitType.Centre, + StartTime = hit_time, + IsStrong = true + })); + + AssertJudgementCount(2); + AssertResult(0, HitResult.Miss); + AssertResult(0, HitResult.IgnoreMiss); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs new file mode 100644 index 0000000000..7bdfcf0b07 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneSwellJudgements.cs @@ -0,0 +1,118 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; + +namespace osu.Game.Rulesets.Taiko.Tests.Judgements +{ + public class TestSceneSwellJudgements : JudgementTest + { + [Test] + public void TestHitAllSwell() + { + const double hit_time = 1000; + + Swell swell = new Swell + { + StartTime = hit_time, + Duration = 1000, + RequiredHits = 10 + }; + + List frames = new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(2001), + }; + + for (int i = 0; i < swell.RequiredHits; i++) + { + double frameTime = 1000 + i * 50; + frames.Add(new TaikoReplayFrame(frameTime, i % 2 == 0 ? TaikoAction.LeftCentre : TaikoAction.LeftRim)); + frames.Add(new TaikoReplayFrame(frameTime + 10)); + } + + PerformTest(frames, CreateBeatmap(swell)); + + AssertJudgementCount(11); + + for (int i = 0; i < swell.RequiredHits; i++) + AssertResult(i, HitResult.IgnoreHit); + + AssertResult(0, HitResult.LargeBonus); + } + + [Test] + public void TestHitSomeSwell() + { + const double hit_time = 1000; + + Swell swell = new Swell + { + StartTime = hit_time, + Duration = 1000, + RequiredHits = 10 + }; + + List frames = new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(2001), + }; + + for (int i = 0; i < swell.RequiredHits / 2; i++) + { + double frameTime = 1000 + i * 50; + frames.Add(new TaikoReplayFrame(frameTime, i % 2 == 0 ? TaikoAction.LeftCentre : TaikoAction.LeftRim)); + frames.Add(new TaikoReplayFrame(frameTime + 10)); + } + + PerformTest(frames, CreateBeatmap(swell)); + + AssertJudgementCount(11); + + for (int i = 0; i < swell.RequiredHits / 2; i++) + AssertResult(i, HitResult.IgnoreHit); + for (int i = swell.RequiredHits / 2; i < swell.RequiredHits; i++) + AssertResult(i, HitResult.IgnoreMiss); + + AssertResult(0, HitResult.IgnoreMiss); + } + + [Test] + public void TestHitNoneSwell() + { + const double hit_time = 1000; + + Swell swell = new Swell + { + StartTime = hit_time, + Duration = 1000, + RequiredHits = 10 + }; + + List frames = new List + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(2001), + }; + + PerformTest(frames, CreateBeatmap(swell)); + + AssertJudgementCount(11); + + for (int i = 0; i < swell.RequiredHits; i++) + AssertResult(i, HitResult.IgnoreMiss); + + AssertResult(0, HitResult.IgnoreMiss); + + AddAssert("all tick offsets are 0", () => JudgementResults.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0)); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs deleted file mode 100644 index bd546b16f2..0000000000 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using System.Linq; -using NUnit.Framework; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Taiko.Objects; - -namespace osu.Game.Rulesets.Taiko.Tests -{ - public class TestSceneSwellJudgements : TestSceneTaikoPlayer - { - [Test] - public void TestZeroTickTimeOffsets() - { - AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted.Value); - AddAssert("all tick offsets are 0", () => Player.Results.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0)); - } - - protected override bool Autoplay => true; - - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) - { - var beatmap = new Beatmap - { - BeatmapInfo = { Ruleset = new TaikoRuleset().RulesetInfo }, - HitObjects = - { - new Swell - { - StartTime = 1000, - Duration = 1000, - } - } - }; - - return beatmap; - } - } -} From c2107bd32277ae7c40e5cf3bb513a44f189be80f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Sep 2022 23:36:27 +0900 Subject: [PATCH 521/532] Fix test failures due to notifications being forwarded before player finishes loading --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index b6c17fbaca..1d101383cc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -301,7 +301,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value, () => Is.EqualTo(1)); - clickNotificationIfAny(); + clickNotification(); AddAssert("check " + volumeName, assert); @@ -370,8 +370,12 @@ namespace osu.Game.Tests.Visual.Gameplay batteryInfo.SetChargeLevel(chargeLevel); })); AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); - AddAssert($"notification {(shouldWarn ? "triggered" : "not triggered")}", () => notificationOverlay.UnreadCount.Value == (shouldWarn ? 1 : 0)); - clickNotificationIfAny(); + + if (shouldWarn) + clickNotification(); + else + AddAssert("notification not triggered", () => notificationOverlay.UnreadCount.Value == 0); + AddUntilStep("wait for player load", () => player.IsLoaded); } @@ -436,9 +440,13 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("skip button not visible", () => !checkSkipButtonVisible()); } - private void clickNotificationIfAny() + private void clickNotification() { - AddStep("click notification", () => notificationOverlay.ChildrenOfType().FirstOrDefault()?.TriggerClick()); + Notification notification = null; + + AddUntilStep("wait for notification", () => (notification = notificationOverlay.ChildrenOfType().FirstOrDefault()) != null); + AddStep("open notification overlay", () => notificationOverlay.Show()); + AddStep("click notification", () => notification.TriggerClick()); } private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(); From 6a371eba5fcc910b627ca299ba09475198c34b04 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Sep 2022 00:12:25 +0900 Subject: [PATCH 522/532] Fix namespace --- osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs index c9e8fefead..7f2f27b2b8 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/JudgementTest.cs @@ -17,7 +17,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Taiko.Tests +namespace osu.Game.Rulesets.Taiko.Tests.Judgements { public class JudgementTest : RateAdjustedBeatmapTestScene { From 7c0e99c5a86c0c28e1399e36c8cdf5a32271c143 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 7 Sep 2022 00:11:51 +0900 Subject: [PATCH 523/532] Decode Geki/Katu from legacy taiko scores into LargeBonus --- .../Formats/LegacyScoreDecoderTest.cs | 18 +++++++++++ .../Resources/Replays/taiko-replay.osr | Bin 0 -> 237 bytes .../Scoring/Legacy/ScoreInfoExtensions.cs | 28 +++++++++++++++--- 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tests/Resources/Replays/taiko-replay.osr diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index aa6fc1f309..cd6e5e7919 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -67,6 +67,24 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeTaikoReplay() + { + var decoder = new TestLegacyScoreDecoder(); + + using (var resourceStream = TestResources.OpenResource("Replays/taiko-replay.osr")) + { + var score = decoder.Parse(resourceStream); + + Assert.AreEqual(1, score.ScoreInfo.Ruleset.OnlineID); + Assert.AreEqual(4, score.ScoreInfo.Statistics[HitResult.Great]); + Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.LargeBonus]); + Assert.AreEqual(4, score.ScoreInfo.MaxCombo); + + Assert.That(score.Replay.Frames, Is.Not.Empty); + } + } + [TestCase(3, true)] [TestCase(6, false)] [TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)] diff --git a/osu.Game.Tests/Resources/Replays/taiko-replay.osr b/osu.Game.Tests/Resources/Replays/taiko-replay.osr new file mode 100644 index 0000000000000000000000000000000000000000..986b3116ab25f62dcb67514b8f52941cddab9b75 GIT binary patch literal 237 zcmZSN=rUpCRxmU*PqQ>jN;EJ_HcqxoN;FS4NKLXxNl8jJvox|WwB+W>P0h|uOvx`U zRpeGMNi{Y$H#9XgPc=7Av^2LcO*A!6v@}XiG&3+yPE9gmVE_RpF!@23fdRzlU|`@D zG%_?ct})OvHr6pUG%%?#)JZt7C`od{EsiLlidY5)1qLgKE(Qj%9;dkG`r7Op-o=T* z*CL8GF^jI?N!zpE@#WrB&!a^VpByr7JWzVO{9Crhvo#`$>L1#cZ&WQ%Oui9fe{Qpf Vr?FkmyS Date: Wed, 7 Sep 2022 13:17:04 +0900 Subject: [PATCH 524/532] Add audio feedback for Esc/Back clearing text from a FocusedTextBox --- .../Graphics/UserInterface/FocusedTextBox.cs | 1 + osu.Game/Graphics/UserInterface/OsuTextBox.cs | 38 +++++++++---------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index 230d921c68..0c18fd36fc 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -88,6 +88,7 @@ namespace osu.Game.Graphics.UserInterface if (Text.Length > 0) { Text = string.Empty; + PlayFeedbackSample(FeedbackSampleType.TextRemove); return true; } } diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 4c2e00d6e0..18977638f3 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -47,7 +47,7 @@ namespace osu.Game.Graphics.UserInterface private bool selectionStarted; private double sampleLastPlaybackTime; - private enum FeedbackSampleType + protected enum FeedbackSampleType { TextAdd, TextAddCaps, @@ -117,30 +117,30 @@ namespace osu.Game.Graphics.UserInterface return; if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples) - playSample(FeedbackSampleType.TextAddCaps); + PlayFeedbackSample(FeedbackSampleType.TextAddCaps); else - playSample(FeedbackSampleType.TextAdd); + PlayFeedbackSample(FeedbackSampleType.TextAdd); } protected override void OnUserTextRemoved(string removed) { base.OnUserTextRemoved(removed); - playSample(FeedbackSampleType.TextRemove); + PlayFeedbackSample(FeedbackSampleType.TextRemove); } protected override void NotifyInputError() { base.NotifyInputError(); - playSample(FeedbackSampleType.TextInvalid); + PlayFeedbackSample(FeedbackSampleType.TextInvalid); } protected override void OnTextCommitted(bool textChanged) { base.OnTextCommitted(textChanged); - playSample(FeedbackSampleType.TextConfirm); + PlayFeedbackSample(FeedbackSampleType.TextConfirm); } protected override void OnCaretMoved(bool selecting) @@ -148,7 +148,7 @@ namespace osu.Game.Graphics.UserInterface base.OnCaretMoved(selecting); if (!selecting) - playSample(FeedbackSampleType.CaretMove); + PlayFeedbackSample(FeedbackSampleType.CaretMove); } protected override void OnTextSelectionChanged(TextSelectionType selectionType) @@ -158,15 +158,15 @@ namespace osu.Game.Graphics.UserInterface switch (selectionType) { case TextSelectionType.Character: - playSample(FeedbackSampleType.SelectCharacter); + PlayFeedbackSample(FeedbackSampleType.SelectCharacter); break; case TextSelectionType.Word: - playSample(selectionStarted ? FeedbackSampleType.SelectCharacter : FeedbackSampleType.SelectWord); + PlayFeedbackSample(selectionStarted ? FeedbackSampleType.SelectCharacter : FeedbackSampleType.SelectWord); break; case TextSelectionType.All: - playSample(FeedbackSampleType.SelectAll); + PlayFeedbackSample(FeedbackSampleType.SelectAll); break; } @@ -179,7 +179,7 @@ namespace osu.Game.Graphics.UserInterface if (!selectionStarted) return; - playSample(FeedbackSampleType.Deselect); + PlayFeedbackSample(FeedbackSampleType.Deselect); selectionStarted = false; } @@ -198,13 +198,13 @@ namespace osu.Game.Graphics.UserInterface case 1: // composition probably ended by pressing backspace, or was cancelled. - playSample(FeedbackSampleType.TextRemove); + PlayFeedbackSample(FeedbackSampleType.TextRemove); return; default: // longer text removed, composition ended because it was cancelled. // could be a different sample if desired. - playSample(FeedbackSampleType.TextRemove); + PlayFeedbackSample(FeedbackSampleType.TextRemove); return; } } @@ -212,7 +212,7 @@ namespace osu.Game.Graphics.UserInterface if (addedTextLength > 0) { // some text was added, probably due to typing new text or by changing the candidate. - playSample(FeedbackSampleType.TextAdd); + PlayFeedbackSample(FeedbackSampleType.TextAdd); return; } @@ -220,14 +220,14 @@ namespace osu.Game.Graphics.UserInterface { // text was probably removed by backspacing. // it's also possible that a candidate that only removed text was changed to. - playSample(FeedbackSampleType.TextRemove); + PlayFeedbackSample(FeedbackSampleType.TextRemove); return; } if (caretMoved) { // only the caret/selection was moved. - playSample(FeedbackSampleType.CaretMove); + PlayFeedbackSample(FeedbackSampleType.CaretMove); } } @@ -238,13 +238,13 @@ namespace osu.Game.Graphics.UserInterface if (successful) { // composition was successfully completed, usually by pressing the enter key. - playSample(FeedbackSampleType.TextConfirm); + PlayFeedbackSample(FeedbackSampleType.TextConfirm); } else { // composition was prematurely ended, eg. by clicking inside the textbox. // could be a different sample if desired. - playSample(FeedbackSampleType.TextConfirm); + PlayFeedbackSample(FeedbackSampleType.TextConfirm); } } @@ -283,7 +283,7 @@ namespace osu.Game.Graphics.UserInterface return samples[RNG.Next(0, samples.Length)]?.GetChannel(); } - private void playSample(FeedbackSampleType feedbackSample) => Schedule(() => + protected void PlayFeedbackSample(FeedbackSampleType feedbackSample) => Schedule(() => { if (Time.Current < sampleLastPlaybackTime + 15) return; From 241d33d415df0bae374889080a3ba0318937ec2f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Sep 2022 14:04:51 +0900 Subject: [PATCH 525/532] Apply NRT to `BeatmapCarousel` --- osu.Game/Screens/Select/BeatmapCarousel.cs | 57 ++++++++++------------ 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 0f130714f1..3370dfbb4e 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Diagnostics; @@ -49,31 +47,31 @@ namespace osu.Game.Screens.Select /// /// Triggered when the loaded change and are completely loaded. /// - public Action BeatmapSetsChanged; + public Action? BeatmapSetsChanged; /// /// The currently selected beatmap. /// - public BeatmapInfo SelectedBeatmapInfo => selectedBeatmap?.BeatmapInfo; + public BeatmapInfo? SelectedBeatmapInfo => selectedBeatmap?.BeatmapInfo; - private CarouselBeatmap selectedBeatmap => selectedBeatmapSet?.Beatmaps.FirstOrDefault(s => s.State.Value == CarouselItemState.Selected); + private CarouselBeatmap? selectedBeatmap => selectedBeatmapSet?.Beatmaps.FirstOrDefault(s => s.State.Value == CarouselItemState.Selected); /// /// The currently selected beatmap set. /// - public BeatmapSetInfo SelectedBeatmapSet => selectedBeatmapSet?.BeatmapSet; + public BeatmapSetInfo? SelectedBeatmapSet => selectedBeatmapSet?.BeatmapSet; /// /// A function to optionally decide on a recommended difficulty from a beatmap set. /// - public Func, BeatmapInfo> GetRecommendedBeatmap; + public Func, BeatmapInfo>? GetRecommendedBeatmap; - private CarouselBeatmapSet selectedBeatmapSet; + private CarouselBeatmapSet? selectedBeatmapSet; /// /// Raised when the is changed. /// - public Action SelectionChanged; + public Action? SelectionChanged; public override bool HandleNonPositionalInput => AllowSelection; public override bool HandlePositionalInput => AllowSelection; @@ -151,15 +149,15 @@ namespace osu.Game.Screens.Select private CarouselRoot root; - private IDisposable subscriptionSets; - private IDisposable subscriptionDeletedSets; - private IDisposable subscriptionBeatmaps; - private IDisposable subscriptionHiddenBeatmaps; + private IDisposable? subscriptionSets; + private IDisposable? subscriptionDeletedSets; + private IDisposable? subscriptionBeatmaps; + private IDisposable? subscriptionHiddenBeatmaps; private readonly DrawablePool setPool = new DrawablePool(100); - private Sample spinSample; - private Sample randomSelectSample; + private Sample? spinSample; + private Sample? randomSelectSample; private int visibleSetsCount; @@ -200,7 +198,7 @@ namespace osu.Game.Screens.Select } [Resolved] - private RealmAccess realm { get; set; } + private RealmAccess realm { get; set; } = null!; protected override void LoadComplete() { @@ -215,7 +213,7 @@ namespace osu.Game.Screens.Select subscriptionHiddenBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => b.Hidden), beatmapsChanged); } - private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet? changes, Exception? error) { // If loading test beatmaps, avoid overwriting with realm subscription callbacks. if (loadedTestBeatmaps) @@ -228,7 +226,7 @@ namespace osu.Game.Screens.Select removeBeatmapSet(sender[i].ID); } - private void beatmapSetsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes, Exception? error) { // If loading test beatmaps, avoid overwriting with realm subscription callbacks. if (loadedTestBeatmaps) @@ -304,7 +302,7 @@ namespace osu.Game.Screens.Select } } - private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes, Exception? error) { // we only care about actual changes in hidden status. if (changes == null) @@ -367,7 +365,7 @@ namespace osu.Game.Screens.Select // check if we can/need to maintain our current selection. if (previouslySelectedID != null) - select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); + select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); } itemsCache.Invalidate(); @@ -384,7 +382,7 @@ namespace osu.Game.Screens.Select /// The beatmap to select. /// Whether to select the beatmap even if it is filtered (i.e., not visible on carousel). /// True if a selection was made, False if it wasn't. - public bool SelectBeatmap(BeatmapInfo beatmapInfo, bool bypassFilters = true) + public bool SelectBeatmap(BeatmapInfo? beatmapInfo, bool bypassFilters = true) { // ensure that any pending events from BeatmapManager have been run before attempting a selection. Scheduler.Update(); @@ -549,7 +547,7 @@ namespace osu.Game.Screens.Select randomSelectSample?.Play(); } - private void select(CarouselItem item) + private void select(CarouselItem? item) { if (!AllowSelection) return; @@ -561,7 +559,7 @@ namespace osu.Game.Screens.Select private FilterCriteria activeCriteria = new FilterCriteria(); - protected ScheduledDelegate PendingFilter; + protected ScheduledDelegate? PendingFilter; public bool AllowSelection = true; @@ -593,7 +591,7 @@ namespace osu.Game.Screens.Select } } - public void Filter(FilterCriteria newCriteria, bool debounce = true) + public void Filter(FilterCriteria? newCriteria, bool debounce = true) { if (newCriteria != null) activeCriteria = newCriteria; @@ -796,7 +794,7 @@ namespace osu.Game.Screens.Select return (firstIndex, lastIndex); } - private CarouselBeatmapSet createCarouselSet(BeatmapSetInfo beatmapSet) + private CarouselBeatmapSet? createCarouselSet(BeatmapSetInfo beatmapSet) { // This can be moved to the realm query if required using: // .Filter("DeletePending == false && Protected == false && ANY Beatmaps.Hidden == false") @@ -962,7 +960,7 @@ namespace osu.Game.Screens.Select /// /// The item to be updated. /// For nested items, the parent of the item to be updated. - private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null) + private void updateItem(DrawableCarouselItem item, DrawableCarouselItem? parent = null) { Vector2 posInScroll = Scroll.ScrollContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre); float itemDrawY = posInScroll.Y - visibleUpperBound; @@ -990,8 +988,7 @@ namespace osu.Game.Screens.Select /// private class CarouselBoundsItem : CarouselItem { - public override DrawableCarouselItem CreateDrawableRepresentation() => - throw new NotImplementedException(); + public override DrawableCarouselItem CreateDrawableRepresentation() => throw new NotImplementedException(); } private class CarouselRoot : CarouselGroupEagerSelect @@ -1017,7 +1014,7 @@ namespace osu.Game.Screens.Select base.AddItem(i); } - public CarouselBeatmapSet RemoveChild(Guid beatmapSetID) + public CarouselBeatmapSet? RemoveChild(Guid beatmapSetID) { if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSet)) { @@ -1039,7 +1036,7 @@ namespace osu.Game.Screens.Select protected override void PerformSelection() { if (LastSelected == null || LastSelected.Filtered.Value) - carousel?.SelectNextRandom(); + carousel.SelectNextRandom(); else base.PerformSelection(); } From e18b524f8e8cd89e94d99d39d324cb0a4d556263 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Sep 2022 14:08:07 +0900 Subject: [PATCH 526/532] Fix missing null checks on `selectedBeatmap` fields in `BeatmapCarousel` --- osu.Game/Screens/Select/BeatmapCarousel.cs | 30 +++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 3370dfbb4e..e0dfbe2a1c 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -264,8 +264,11 @@ namespace osu.Game.Screens.Select foreach (int i in changes.InsertedIndices) UpdateBeatmapSet(sender[i].Detach()); - if (changes.DeletedIndices.Length > 0) + if (changes.DeletedIndices.Length > 0 && SelectedBeatmapInfo != null) { + // If SelectedBeatmapInfo is non-null, the set should also be non-null. + Debug.Assert(SelectedBeatmapSet != null); + // To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions. // When an update occurs, the previous beatmap set is either soft or hard deleted. // Check if the current selection was potentially deleted by re-querying its validity. @@ -440,6 +443,9 @@ namespace osu.Game.Screens.Select private void selectNextSet(int direction, bool skipDifficulties) { + if (selectedBeatmap == null || selectedBeatmapSet == null) + return; + var unfilteredSets = beatmapSets.Where(s => !s.Filtered.Value).ToList(); var nextSet = unfilteredSets[(unfilteredSets.IndexOf(selectedBeatmapSet) + direction + unfilteredSets.Count) % unfilteredSets.Count]; @@ -452,7 +458,7 @@ namespace osu.Game.Screens.Select private void selectNextDifficulty(int direction) { - if (selectedBeatmap == null) + if (selectedBeatmap == null || selectedBeatmapSet == null) return; var unfilteredDifficulties = selectedBeatmapSet.Items.Where(s => !s.Filtered.Value).ToList(); @@ -481,7 +487,7 @@ namespace osu.Game.Screens.Select if (!visibleSets.Any()) return false; - if (selectedBeatmap != null) + if (selectedBeatmap != null && selectedBeatmapSet != null) { randomSelectedBeatmaps.Push(selectedBeatmap); @@ -524,11 +530,13 @@ namespace osu.Game.Screens.Select if (!beatmap.Filtered.Value) { - if (RandomAlgorithm.Value == RandomSelectAlgorithm.RandomPermutation) - previouslyVisitedRandomSets.Remove(selectedBeatmapSet); - if (selectedBeatmapSet != null) + { + if (RandomAlgorithm.Value == RandomSelectAlgorithm.RandomPermutation) + previouslyVisitedRandomSets.Remove(selectedBeatmapSet); + playSpinSample(distanceBetween(beatmap, selectedBeatmapSet)); + } select(beatmap); break; @@ -540,9 +548,13 @@ namespace osu.Game.Screens.Select private void playSpinSample(double distance) { - var chan = spinSample.GetChannel(); - chan.Frequency.Value = 1f + Math.Min(1f, distance / visibleSetsCount); - chan.Play(); + var chan = spinSample?.GetChannel(); + + if (chan != null) + { + chan.Frequency.Value = 1f + Math.Min(1f, distance / visibleSetsCount); + chan.Play(); + } randomSelectSample?.Play(); } From f3bda4e040196f38763001768922f3c2553b6dd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Sep 2022 14:27:24 +0900 Subject: [PATCH 527/532] Fix weird edge case of nullability in `CarouselRoot` A bit unfortunately, but it's what we get for having ctor level bindings.. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index e0dfbe2a1c..a8cb06b888 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1005,7 +1005,8 @@ namespace osu.Game.Screens.Select private class CarouselRoot : CarouselGroupEagerSelect { - private readonly BeatmapCarousel carousel; + // May only be null during construction (State.Value set causes PerformSelection to be triggered). + private readonly BeatmapCarousel? carousel; public readonly Dictionary BeatmapSetsByID = new Dictionary(); @@ -1048,7 +1049,7 @@ namespace osu.Game.Screens.Select protected override void PerformSelection() { if (LastSelected == null || LastSelected.Filtered.Value) - carousel.SelectNextRandom(); + carousel?.SelectNextRandom(); else base.PerformSelection(); } From 866bc553fe65a8720c82c804f36a7f97c6a3f30c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Sep 2022 15:30:48 +0900 Subject: [PATCH 528/532] Tidy up `TestSceneStoryboard` --- .../Visual/Gameplay/TestSceneStoryboard.cs | 54 ++++++++----------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 2ec675874b..eaf22ba9cc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -24,8 +22,9 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public class TestSceneStoryboard : OsuTestScene { - private Container storyboardContainer; - private DrawableStoryboard storyboard; + private Container storyboardContainer = null!; + + private DrawableStoryboard? storyboard; [Test] public void TestStoryboard() @@ -40,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestStoryboardMissingVideo() { - AddStep("Load storyboard with missing video", loadStoryboardNoVideo); + AddStep("Load storyboard with missing video", () => loadStoryboard("storyboard_no_video.osu")); } [BackgroundDependencyLoader] @@ -77,18 +76,18 @@ namespace osu.Game.Tests.Visual.Gameplay Beatmap.BindValueChanged(beatmapChanged, true); } - private void beatmapChanged(ValueChangedEvent e) => loadStoryboard(e.NewValue); + private void beatmapChanged(ValueChangedEvent e) => loadStoryboard(e.NewValue.Storyboard); private void restart() { var track = Beatmap.Value.Track; track.Reset(); - loadStoryboard(Beatmap.Value); + loadStoryboard(Beatmap.Value.Storyboard); track.Start(); } - private void loadStoryboard(IWorkingBeatmap working) + private void loadStoryboard(Storyboard toLoad) { if (storyboard != null) storyboardContainer.Remove(storyboard, true); @@ -96,34 +95,25 @@ namespace osu.Game.Tests.Visual.Gameplay var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; storyboardContainer.Clock = decoupledClock; - storyboard = working.Storyboard.CreateDrawable(SelectedMods.Value); + storyboard = toLoad.CreateDrawable(SelectedMods.Value); storyboard.Passing = false; - storyboardContainer.Add(storyboard); - decoupledClock.ChangeSource(working.Track); - } - - private void loadStoryboardNoVideo() - { - if (storyboard != null) - storyboardContainer.Remove(storyboard, true); - - var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; - storyboardContainer.Clock = decoupledClock; - - Storyboard sb; - - using (var str = TestResources.OpenResource("storyboard_no_video.osu")) - using (var bfr = new LineBufferedReader(str)) - { - var decoder = new LegacyStoryboardDecoder(); - sb = decoder.Decode(bfr); - } - - storyboard = sb.CreateDrawable(SelectedMods.Value); - storyboardContainer.Add(storyboard); decoupledClock.ChangeSource(Beatmap.Value.Track); } + + private void loadStoryboard(string filename) + { + Storyboard loaded; + + using (var str = TestResources.OpenResource(filename)) + using (var bfr = new LineBufferedReader(str)) + { + var decoder = new LegacyStoryboardDecoder(); + loaded = decoder.Decode(bfr); + } + + loadStoryboard(loaded); + } } } From 258b8f015c9dfda22b7d1720c61347c7ba449411 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Sep 2022 15:37:22 +0900 Subject: [PATCH 529/532] Add test coverage of storyboard using zero `VectorScale` --- .../Gameplay/TestSceneDrawableStoryboardSprite.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs index ca7d7b42d8..34723e3329 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -73,6 +71,17 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight)); } + [Test] + public void TestZeroScale() + { + const string lookup_name = "hitcircleoverlay"; + + AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); + AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); + AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(0, 1))); + AddAssert("zero width", () => sprites.All(s => s.ScreenSpaceDrawQuad.Width == 0)); + } + [Test] public void TestNegativeScale() { From 824e68dab35f114c1960c5a5c30dacbbe3c99896 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Sep 2022 15:16:20 +0900 Subject: [PATCH 530/532] Fix `VectorScale` set to 0 still showing some sprites in storyboard This implementation was matching the [framework side implementation of scale](https://github.com/ppy/osu-framework/blob/16d1c2d3356b0f59271fb49cff5fe8373ae9cdf3/osu.Framework/Graphics/Drawable.cs#L973-L976) but I don't think it's required here. I'm still not sure if the framework implementation is correct, but removing it locally does seem to fix broken storyboard cases. Closes https://github.com/ppy/osu/issues/20155. --- .../Storyboards/Drawables/DrawableStoryboardAnimation.cs | 5 ----- osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 5 ----- 2 files changed, 10 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 9822f36620..f3187d77b7 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -56,11 +56,6 @@ namespace osu.Game.Storyboards.Drawables get => vectorScale; set { - if (Math.Abs(value.X) < Precision.FLOAT_EPSILON) - value.X = Precision.FLOAT_EPSILON; - if (Math.Abs(value.Y) < Precision.FLOAT_EPSILON) - value.Y = Precision.FLOAT_EPSILON; - if (vectorScale == value) return; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 28ed2e65e3..b86b021d51 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -55,11 +55,6 @@ namespace osu.Game.Storyboards.Drawables get => vectorScale; set { - if (Math.Abs(value.X) < Precision.FLOAT_EPSILON) - value.X = Precision.FLOAT_EPSILON; - if (Math.Abs(value.Y) < Precision.FLOAT_EPSILON) - value.Y = Precision.FLOAT_EPSILON; - if (vectorScale == value) return; From b50116e9e4c29b33130712783e2625ee7a391d4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Sep 2022 14:40:51 +0900 Subject: [PATCH 531/532] Add missing null check in `BeatmapCarousel` tests --- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index c9e63fa621..0e72463d1e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.SongSelect if (isIterating) AddUntilStep("selection changed", () => !carousel.SelectedBeatmapInfo?.Equals(selection) == true); else - AddUntilStep("selection not changed", () => carousel.SelectedBeatmapInfo.Equals(selection)); + AddUntilStep("selection not changed", () => carousel.SelectedBeatmapInfo?.Equals(selection) == true); } } } @@ -382,7 +382,7 @@ namespace osu.Game.Tests.Visual.SongSelect // buffer the selection setSelected(3, 2); - AddStep("get search text", () => searchText = carousel.SelectedBeatmapSet.Metadata.Title); + AddStep("get search text", () => searchText = carousel.SelectedBeatmapSet!.Metadata.Title); setSelected(1, 3); @@ -701,7 +701,7 @@ namespace osu.Game.Tests.Visual.SongSelect setSelected(2, 1); AddAssert("Selection is non-null", () => currentSelection != null); - AddStep("Remove selected", () => carousel.RemoveBeatmapSet(carousel.SelectedBeatmapSet)); + AddStep("Remove selected", () => carousel.RemoveBeatmapSet(carousel.SelectedBeatmapSet!)); waitForSelection(2); AddStep("Remove first", () => carousel.RemoveBeatmapSet(carousel.BeatmapSets.First())); @@ -804,7 +804,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false)); AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false)); - AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmapInfo.Ruleset.OnlineID == 0); + AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 0); AddStep("remove mixed set", () => { @@ -854,7 +854,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Restore no filter", () => { carousel.Filter(new FilterCriteria(), false); - eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID); + eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID); }); } @@ -899,10 +899,10 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Restore different ruleset filter", () => { carousel.Filter(new FilterCriteria { Ruleset = rulesets.GetRuleset(1) }, false); - eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID); + eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID); }); - AddAssert("selection changed", () => !carousel.SelectedBeatmapInfo.Equals(manySets.First().Beatmaps.First())); + AddAssert("selection changed", () => !carousel.SelectedBeatmapInfo!.Equals(manySets.First().Beatmaps.First())); } AddAssert("Selection was random", () => eagerSelectedIDs.Count > 2); From b5b66de3c9c8a3a99b6e9ffd6d4994a3fa6e6b61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Sep 2022 17:45:15 +0900 Subject: [PATCH 532/532] Fix target mod crashing if beatmap is played with a break after all hitobjects Closes https://github.com/ppy/osu/issues/20161. --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 5e2a92e5e9..861ad80b7f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -362,10 +362,12 @@ namespace osu.Game.Rulesets.Osu.Mods { return breaks.Any(breakPeriod => { - var firstObjAfterBreak = originalHitObjects.First(obj => almostBigger(obj.StartTime, breakPeriod.EndTime)); + OsuHitObject? firstObjAfterBreak = originalHitObjects.FirstOrDefault(obj => almostBigger(obj.StartTime, breakPeriod.EndTime)); return almostBigger(time, breakPeriod.StartTime) - && definitelyBigger(firstObjAfterBreak.StartTime, time); + // There should never really be a break section with no objects after it, but we've seen crashes from users with malformed beatmaps, + // so it's best to guard against this. + && (firstObjAfterBreak == null || definitelyBigger(firstObjAfterBreak.StartTime, time)); }); }