From 79a3afe06feffe9db9aa60760a1509b01bfee3ba Mon Sep 17 00:00:00 2001
From: Jay Lawton
Date: Thu, 19 Dec 2024 01:16:27 +1000
Subject: [PATCH 001/262] Implement considerations for Relax within osu!taiko
diffcalc (#30591)
---
.../Difficulty/TaikoDifficultyCalculator.cs | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index 7f2558c406..b3efb7f46d 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -77,6 +77,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (beatmap.HitObjects.Count == 0)
return new TaikoDifficultyAttributes { Mods = mods };
+ bool isRelax = mods.Any(h => h is TaikoModRelax);
+
Colour colour = (Colour)skills.First(x => x is Colour);
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
@@ -88,15 +90,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier;
double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5);
- double combinedRating = combinedDifficultyValue(rhythm, colour, stamina);
+ double combinedRating = combinedDifficultyValue(rhythm, colour, stamina, isRelax);
double starRating = rescale(combinedRating * 1.4);
// 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.925;
- // For maps with low colour variance and high stamina requirement, multiple inputs are more likely to be abused.
- if (colourRating < 2 && staminaRating > 8)
+
+ // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused.
+ if (isRelax)
+ starRating *= 0.60;
+ else if (colourRating < 2 && staminaRating > 8)
starRating *= 0.80;
}
@@ -138,7 +143,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 combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina)
+ private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina, bool isRelax)
{
List peaks = new List();
@@ -152,6 +157,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
+ if (isRelax)
+ {
+ colourPeak = 0; // There is no colour difficulty in relax.
+ staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count.
+ }
+
double peak = norm(1.5, colourPeak, staminaPeak);
peak = norm(2, peak, rhythmPeak);
From 0f2f25db532418ff5f8deba221ef14ed7a4867e7 Mon Sep 17 00:00:00 2001
From: YaniFR <58740803+YaniFR@users.noreply.github.com>
Date: Wed, 18 Dec 2024 19:11:51 +0100
Subject: [PATCH 002/262] Adjust `DifficultyValue` curve to avoid lower star
rating of osu!taiko being too inflated (#31067)
* low sr
* merge two line
* update decimal
* fix formatting
---------
Co-authored-by: StanR
---
.../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 c672b7a1d9..ed7d41bf72 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -73,7 +73,8 @@ 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.115) - 4.0, 2.25) / 1150.0;
+ double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0;
+ double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1150.0);
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
difficultyValue *= lengthBonus;
From 4ca88ae2d66dd417a8380144b5fe5010821ad9ec Mon Sep 17 00:00:00 2001
From: Jay Lawton
Date: Thu, 19 Dec 2024 21:32:59 +1000
Subject: [PATCH 003/262] Refactor `TaikoDifficultyCalculator` and add
`DifficultStrain` attributes (#31191)
* refactor + countdifficultstrain
* norm in utils
* adjust scaling shift
* fix comment
* revert all value changes
* add the else back
* remove cds comments
---
.../Difficulty/TaikoDifficultyAttributes.cs | 13 ++--
.../Difficulty/TaikoDifficultyCalculator.cs | 75 ++++++++++---------
.../Utils/DifficultyCalculationUtils.cs | 9 +++
3 files changed, 58 insertions(+), 39 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
index c8f0448767..4a35c30e60 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
@@ -34,11 +34,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
[JsonProperty("colour_difficulty")]
public double ColourDifficulty { get; set; }
- ///
- /// The difficulty corresponding to the hardest parts of the map.
- ///
- [JsonProperty("peak_difficulty")]
- public double PeakDifficulty { get; set; }
+ [JsonProperty("rhythm_difficult_strains")]
+ public double RhythmTopStrains { get; set; }
+
+ [JsonProperty("colour_difficult_strains")]
+ public double ColourTopStrains { get; set; }
+
+ [JsonProperty("stamina_difficult_strains")]
+ public double StaminaTopStrains { 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 b3efb7f46d..05081d471e 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -8,6 +8,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
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.Scoring;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
@@ -53,18 +54,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
- List difficultyHitObjects = new List();
- List centreObjects = new List();
- List rimObjects = new List();
- List noteObjects = new List();
+ var difficultyHitObjects = new List();
+ var centreObjects = new List();
+ var rimObjects = new List();
+ var noteObjects = new List();
+ // Generate TaikoDifficultyHitObjects from the beatmap's hit objects.
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)
- );
+ 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);
@@ -79,28 +87,33 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
bool isRelax = mods.Any(h => h is TaikoModRelax);
- Colour colour = (Colour)skills.First(x => x is Colour);
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
+ Colour colour = (Colour)skills.First(x => x is Colour);
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina);
- double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
+ double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier;
double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5);
+ double rhythmDifficultStrains = rhythm.CountTopWeightedStrains();
+ double colourDifficultStrains = colour.CountTopWeightedStrains();
+ double staminaDifficultStrains = stamina.CountTopWeightedStrains();
+
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina, isRelax);
double starRating = rescale(combinedRating * 1.4);
- // TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system.
+ // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope.
if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0)
{
starRating *= 0.925;
- // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused.
+ // For maps with relax, multiple inputs are more likely to be abused.
if (isRelax)
starRating *= 0.60;
+ // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused.
else if (colourRating < 2 && staminaRating > 8)
starRating *= 0.80;
}
@@ -112,11 +125,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
StarRating = starRating,
Mods = mods,
- StaminaDifficulty = staminaRating,
- MonoStaminaFactor = monoStaminaFactor,
RhythmDifficulty = rhythmRating,
ColourDifficulty = colourRating,
- PeakDifficulty = combinedRating,
+ StaminaDifficulty = staminaRating,
+ MonoStaminaFactor = monoStaminaFactor,
+ StaminaTopStrains = staminaDifficultStrains,
+ RhythmTopStrains = rhythmDifficultStrains,
+ ColourTopStrains = colourDifficultStrains,
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
MaxCombo = beatmap.GetMaxCombo(),
@@ -125,17 +140,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return attributes;
}
- ///
- /// Applies a final re-scaling of the star rating.
- ///
- /// The raw star rating value before re-scaling.
- private double rescale(double sr)
- {
- if (sr < 0) return sr;
-
- return 10.43 * Math.Log(sr / 8 + 1);
- }
-
///
/// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
///
@@ -153,8 +157,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
for (int i = 0; i < colourPeaks.Count; i++)
{
- double colourPeak = colourPeaks[i] * colour_skill_multiplier;
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
+ double colourPeak = colourPeaks[i] * colour_skill_multiplier;
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
if (isRelax)
@@ -163,8 +167,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count.
}
- double peak = norm(1.5, colourPeak, staminaPeak);
- peak = norm(2, peak, rhythmPeak);
+ double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), 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.
@@ -185,10 +188,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
}
///
- /// Returns the p-norm of an n-dimensional vector.
+ /// Applies a final re-scaling of the star rating.
///
- /// 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);
+ /// The raw star rating value before re-scaling.
+ private double rescale(double sr)
+ {
+ if (sr < 0) return sr;
+
+ return 10.43 * Math.Log(sr / 8 + 1);
+ }
}
}
diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
index b9efcd683d..df2d84d6f2 100644
--- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
+++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
namespace osu.Game.Rulesets.Difficulty.Utils
{
@@ -46,5 +47,13 @@ namespace osu.Game.Rulesets.Difficulty.Utils
/// Exponent
/// The output of logistic function
public static double Logistic(double exponent, double maxValue = 1) => maxValue / (1 + Math.Exp(exponent));
+
+ ///
+ /// Returns the p-norm of an n-dimensional vector (https://en.wikipedia.org/wiki/Norm_(mathematics))
+ ///
+ /// The value of p to calculate the norm for.
+ /// The coefficients of the vector.
+ /// The p-norm of the vector.
+ public static double Norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
}
}
From ecd6b4192816391591ca8e96b77d80fe7c1fa948 Mon Sep 17 00:00:00 2001
From: Jay Lawton
Date: Fri, 20 Dec 2024 00:45:11 +1000
Subject: [PATCH 004/262] Increase `accscalingshift` and include `countok` in
hit proportion (#31195)
* revert acc scaling shift to previous values
* increase variance in accuracy values across od
* move return values, move nullcheck into return
---------
Co-authored-by: James Wilson
---
.../Difficulty/TaikoPerformanceCalculator.cs | 17 +++++++----------
1 file changed, 7 insertions(+), 10 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index ed7d41bf72..a93f4c66ab 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
// Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps.
double accScalingExponent = 2 + attributes.MonoStaminaFactor;
- double accScalingShift = 300 - 100 * attributes.MonoStaminaFactor;
+ double accScalingShift = 400 - 100 * attributes.MonoStaminaFactor;
return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent);
}
@@ -134,6 +134,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
+ double? deviationGreatWindow = calcDeviationGreatWindow();
+ double? deviationGoodWindow = calcDeviationGoodWindow();
+
+ return deviationGreatWindow is null ? deviationGoodWindow : Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value);
+
// The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window.
double? calcDeviationGreatWindow()
{
@@ -160,7 +165,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double n = totalHits;
// Proportion of greats + goods hit.
- double p = totalSuccessfulHits / n;
+ double p = Math.Max(0, totalSuccessfulHits - 0.0005 * countOk) / n;
// We can be 99% confident that p is at least this value.
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
@@ -168,14 +173,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
// We can be 99% confident that the deviation is not higher than:
return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
}
-
- double? deviationGreatWindow = calcDeviationGreatWindow();
- double? deviationGoodWindow = calcDeviationGoodWindow();
-
- if (deviationGreatWindow is null)
- return deviationGoodWindow;
-
- return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value);
}
private int totalHits => countGreat + countOk + countMeh + countMiss;
From d8c3d899ebb4660b97301f9a5d07902bb4598cbe Mon Sep 17 00:00:00 2001
From: Jay Lawton
Date: Fri, 20 Dec 2024 03:22:16 +1000
Subject: [PATCH 005/262] remove particular condition on convert nerf (#31196)
Co-authored-by: James Wilson
---
.../Difficulty/TaikoDifficultyCalculator.cs | 3 ---
1 file changed, 3 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index 05081d471e..8f725d4f94 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -113,9 +113,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
// For maps with relax, multiple inputs are more likely to be abused.
if (isRelax)
starRating *= 0.60;
- // For maps with either relax or low colour variance and high stamina requirement, multiple inputs are more likely to be abused.
- else if (colourRating < 2 && staminaRating > 8)
- starRating *= 0.80;
}
HitWindows hitWindows = new TaikoHitWindows();
From f722f94f26f0055f7a68bb867b60600aff6bac81 Mon Sep 17 00:00:00 2001
From: StanR
Date: Sat, 21 Dec 2024 04:32:51 +0500
Subject: [PATCH 006/262] Simplify osu! high-bpm acute angle jumps bonus
(#30902)
* Simplify osu! high-bpm acute angle jumps bonus
* Add aim wiggle bonus
* Add hitwindow-based aim velocity decrease
* Revert "Add hitwindow-based aim velocity decrease"
This reverts commit bcebe9662cfcb7a72805e48712525ef54ec9820e.
* Move wiggle multiplier to a const, slightly decrease acute bonus multiplier
* Make sure the previous object in the wiggle bonus is also part of the wiggle
* Scale the wiggle bonus multiplayer down
* Increase the acute angle jump bonus multiplier
* Make wiggle bonus only apply on >150 bpm streams, make repetitive angle penalty
* Reduce wiggle bonus multiplier to not break velocity>difficulty relation
* Adjust wiggle falloff function to fix stability issues
* Adjust wiggle consts
* Update tests
---
.../OsuDifficultyCalculatorTest.cs | 6 ++--
.../Difficulty/Evaluators/AimEvaluator.cs | 33 ++++++++++++-------
.../Utils/DifficultyCalculationUtils.cs | 24 ++++++++++++++
3 files changed, 48 insertions(+), 15 deletions(-)
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index efda3fa369..9798611488 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,20 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
- [TestCase(6.7171144000821119d, 239, "diffcalc-test")]
+ [TestCase(6.718709884850683d, 239, "diffcalc-test")]
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
[TestCase(0.14143808967817237d, 2, "nan-slider")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(8.9825709931204205d, 239, "diffcalc-test")]
+ [TestCase(9.4310274277499619d, 239, "diffcalc-test")]
[TestCase(1.7550169162648608d, 54, "zero-length-sliders")]
[TestCase(0.55231632896800109d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
- [TestCase(6.7171144000821119d, 239, "diffcalc-test")]
+ [TestCase(6.718709884850683d, 239, "diffcalc-test")]
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index 9816f6d0a4..c3270f25f8 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -12,9 +12,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
public static class AimEvaluator
{
private const double wide_angle_multiplier = 1.5;
- private const double acute_angle_multiplier = 1.95;
+ private const double acute_angle_multiplier = 2.35;
private const double slider_multiplier = 1.35;
private const double velocity_change_multiplier = 0.75;
+ private const double wiggle_multiplier = 1.02;
///
/// Evaluates the difficulty of aiming the current object, based on:
@@ -64,6 +65,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double acuteAngleBonus = 0;
double sliderBonus = 0;
double velocityChangeBonus = 0;
+ double wiggleBonus = 0;
double aimStrain = currVelocity; // Start strain with regular velocity.
@@ -79,22 +81,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double angleBonus = Math.Min(currVelocity, prevVelocity);
wideAngleBonus = calcWideAngleBonus(currAngle);
- acuteAngleBonus = calcAcuteAngleBonus(currAngle);
- if (DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2) < 300) // Only buff deltaTime exceeding 300 bpm 1/2.
- acuteAngleBonus = 0;
- else
- {
- acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
- * Math.Min(angleBonus, diameter * 1.25 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
- * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
- * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, radius, diameter) - radius) / radius), 2); // Buff distance exceeding radius up to diameter.
- }
+ // Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
+ acuteAngleBonus = calcAcuteAngleBonus(currAngle) *
+ angleBonus *
+ DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) *
+ DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)));
// Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse.
- acuteAngleBonus *= 0.5 + 0.5 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3)));
+ acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3)));
+
+ // Apply wiggle bonus for jumps that are [radius, 2*diameter] in distance, with < 110 angle and bpm > 150
+ // https://www.desmos.com/calculator/iis7lgbppe
+ wiggleBonus = angleBonus
+ * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter)
+ * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
+ * DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
+ * DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter)
+ * Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
+ * DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));
}
}
@@ -122,6 +129,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
}
+ aimStrain += wiggleBonus * wiggle_multiplier;
+
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier);
diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
index df2d84d6f2..055d8a458b 100644
--- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
+++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
@@ -55,5 +55,29 @@ namespace osu.Game.Rulesets.Difficulty.Utils
/// The coefficients of the vector.
/// The p-norm of the vector.
public static double Norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
+
+ ///
+ /// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations)
+ ///
+ /// Value to calculate the function for
+ /// Value at which function returns 0
+ /// Value at which function returns 1
+ public static double Smootherstep(double x, double start, double end)
+ {
+ x = Math.Clamp((x - start) / (end - start), 0.0, 1.0);
+
+ return x * x * x * (x * (6.0 * x - 15.0) + 10.0);
+ }
+
+ ///
+ /// Reverse linear interpolation function (https://en.wikipedia.org/wiki/Linear_interpolation)
+ ///
+ /// Value to calculate the function for
+ /// Value at which function returns 0
+ /// Value at which function returns 1
+ public static double ReverseLerp(double x, double start, double end)
+ {
+ return Math.Clamp((x - start) / (end - start), 0.0, 1.0);
+ }
}
}
From f6a36f7b2e1427f858b087052bfe7f3dc50b2ab2 Mon Sep 17 00:00:00 2001
From: Jay Lawton
Date: Sat, 21 Dec 2024 20:19:14 +1000
Subject: [PATCH 007/262] Implement `Reading` Skill into osu!taiko (#31208)
---
.../Difficulty/Evaluators/ReadingEvaluator.cs | 43 ++++++++++++++++
.../Preprocessing/Reading/EffectiveBPM.cs | 50 +++++++++++++++++++
.../Preprocessing/TaikoDifficultyHitObject.cs | 10 ++++
.../Difficulty/Skills/Reading.cs | 44 ++++++++++++++++
.../Difficulty/TaikoDifficultyAttributes.cs | 6 +++
.../Difficulty/TaikoDifficultyCalculator.cs | 22 +++++---
.../Difficulty/TaikoPerformanceCalculator.cs | 3 --
7 files changed, 169 insertions(+), 9 deletions(-)
create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs
create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs
create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs
new file mode 100644
index 0000000000..a6a1513842
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs
@@ -0,0 +1,43 @@
+// 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.Utils;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
+{
+ public static class ReadingEvaluator
+ {
+ private readonly struct VelocityRange
+ {
+ public double Min { get; }
+ public double Max { get; }
+ public double Center => (Max + Min) / 2;
+ public double Range => Max - Min;
+
+ public VelocityRange(double min, double max)
+ {
+ Min = min;
+ Max = max;
+ }
+ }
+
+ ///
+ /// Calculates the influence of higher slider velocities on hitobject difficulty.
+ /// The bonus is determined based on the EffectiveBPM, shifting within a defined range
+ /// between the upper and lower boundaries to reflect how increased slider velocity impacts difficulty.
+ ///
+ /// The hit object to evaluate.
+ /// The reading difficulty value for the given hit object.
+ public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject)
+ {
+ double effectiveBPM = noteObject.EffectiveBPM;
+
+ var highVelocity = new VelocityRange(480, 640);
+ var midVelocity = new VelocityRange(360, 480);
+
+ return 1.0 * DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center, 1.0 / (highVelocity.Range / 10))
+ + 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs
new file mode 100644
index 0000000000..17e05d5fbf
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading
+{
+ public class EffectiveBPMPreprocessor
+ {
+ private readonly IList noteObjects;
+ private readonly double globalSliderVelocity;
+
+ public EffectiveBPMPreprocessor(IBeatmap beatmap, List noteObjects)
+ {
+ this.noteObjects = noteObjects;
+ globalSliderVelocity = beatmap.Difficulty.SliderMultiplier;
+ }
+
+ ///
+ /// Calculates and sets the effective BPM and slider velocity for each note object, considering clock rate and scroll speed.
+ ///
+ public void ProcessEffectiveBPM(ControlPointInfo controlPointInfo, double clockRate)
+ {
+ foreach (var currentNoteObject in noteObjects)
+ {
+ double startTime = currentNoteObject.StartTime * clockRate;
+
+ // Retrieve the timing point at the note's start time
+ TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime);
+
+ // Calculate the slider velocity at the note's start time.
+ double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, startTime, clockRate);
+ currentNoteObject.CurrentSliderVelocity = currentSliderVelocity;
+
+ currentNoteObject.EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity;
+ }
+ }
+
+ ///
+ /// Calculates the slider velocity based on control point info and clock rate.
+ ///
+ private double calculateSliderVelocity(ControlPointInfo controlPointInfo, double startTime, double clockRate)
+ {
+ var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime);
+ return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
index 4aaee50c18..e741e4c9e7 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
@@ -48,6 +48,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
///
public readonly TaikoDifficultyHitObjectColour Colour;
+ ///
+ /// The adjusted BPM of this hit object, based on its slider velocity and scroll speed.
+ ///
+ public double EffectiveBPM;
+
+ ///
+ /// The current slider velocity of this hit object.
+ ///
+ public double CurrentSliderVelocity;
+
///
/// Creates a new difficulty hit object.
///
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs
new file mode 100644
index 0000000000..9de058f289
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.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.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
+{
+ ///
+ /// Calculates the reading coefficient of taiko difficulty.
+ ///
+ public class Reading : StrainDecaySkill
+ {
+ protected override double SkillMultiplier => 1.0;
+ protected override double StrainDecayBase => 0.4;
+
+ private double currentStrain;
+
+ public Reading(Mod[] mods)
+ : base(mods)
+ {
+ }
+
+ protected override double StrainValueOf(DifficultyHitObject current)
+ {
+ // Drum Rolls and Swells are exempt.
+ if (current.BaseObject is not Hit)
+ {
+ return 0.0;
+ }
+
+ var taikoObject = (TaikoDifficultyHitObject)current;
+
+ currentStrain *= StrainDecayBase;
+ currentStrain += ReadingEvaluator.EvaluateDifficultyOf(taikoObject) * SkillMultiplier;
+
+ return currentStrain;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
index 4a35c30e60..d3cdb379d5 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
@@ -28,6 +28,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
[JsonProperty("rhythm_difficulty")]
public double RhythmDifficulty { get; set; }
+ ///
+ /// The difficulty corresponding to the reading skill.
+ ///
+ [JsonProperty("reading_difficulty")]
+ public double ReadingDifficulty { get; set; }
+
///
/// The difficulty corresponding to the colour skill.
///
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index 8f725d4f94..0d6ecb8d3e 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.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.Preprocessing.Reading;
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Scoring;
@@ -22,7 +23,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
public class TaikoDifficultyCalculator : DifficultyCalculator
{
private const double difficulty_multiplier = 0.084375;
- private const double rhythm_skill_multiplier = 0.2 * difficulty_multiplier;
+ private const double rhythm_skill_multiplier = 0.200 * difficulty_multiplier;
+ private const double reading_skill_multiplier = 0.100 * difficulty_multiplier;
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier;
@@ -38,6 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return new Skill[]
{
new Rhythm(mods),
+ new Reading(mods),
new Colour(mods),
new Stamina(mods, false),
new Stamina(mods, true)
@@ -58,6 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
var centreObjects = new List();
var rimObjects = new List();
var noteObjects = new List();
+ EffectiveBPMPreprocessor bpmLoader = new EffectiveBPMPreprocessor(beatmap, noteObjects);
// Generate TaikoDifficultyHitObjects from the beatmap's hit objects.
for (int i = 2; i < beatmap.HitObjects.Count; i++)
@@ -76,6 +80,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
}
TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects);
+ bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate);
return difficultyHitObjects;
}
@@ -88,11 +93,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
bool isRelax = mods.Any(h => h is TaikoModRelax);
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
+ Reading reading = (Reading)skills.First(x => x is Reading);
Colour colour = (Colour)skills.First(x => x is Colour);
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina);
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
+ double readingRating = reading.DifficultyValue() * reading_skill_multiplier;
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier;
@@ -102,13 +109,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double colourDifficultStrains = colour.CountTopWeightedStrains();
double staminaDifficultStrains = stamina.CountTopWeightedStrains();
- double combinedRating = combinedDifficultyValue(rhythm, colour, stamina, isRelax);
+ double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax);
double starRating = rescale(combinedRating * 1.4);
// Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope.
if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0)
{
- starRating *= 0.925;
+ starRating *= 0.825;
// For maps with relax, multiple inputs are more likely to be abused.
if (isRelax)
@@ -123,6 +130,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
StarRating = starRating,
Mods = mods,
RhythmDifficulty = rhythmRating,
+ ReadingDifficulty = readingRating,
ColourDifficulty = colourRating,
StaminaDifficulty = staminaRating,
MonoStaminaFactor = monoStaminaFactor,
@@ -144,17 +152,19 @@ 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 combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina, bool isRelax)
+ private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax)
{
List peaks = new List();
- var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
+ var readingPeaks = reading.GetCurrentStrainPeaks().ToList();
+ var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
for (int i = 0; i < colourPeaks.Count; i++)
{
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
+ double readingPeak = readingPeaks[i] * reading_skill_multiplier;
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
@@ -164,7 +174,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count.
}
- double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak);
+ double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak, readingPeak);
// 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.
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index a93f4c66ab..5da18e7963 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -87,9 +87,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (score.Mods.Any(m => m is ModHidden))
difficultyValue *= 1.025;
- if (score.Mods.Any(m => m is ModHardRock))
- difficultyValue *= 1.10;
-
if (score.Mods.Any(m => m is ModFlashlight))
difficultyValue *= Math.Max(1, 1.050 - Math.Min(attributes.MonoStaminaFactor / 50, 1) * lengthBonus);
From 6808a5a77cffdb5800fc6443823bcad80283f549 Mon Sep 17 00:00:00 2001
From: StanR
Date: Sun, 22 Dec 2024 04:45:29 +0500
Subject: [PATCH 008/262] Change slider drop penalty to use actual number of
difficult sliders, fix slider drop penalty being too lenient (#31055)
* Change slider drop penalty to use actual number of difficult sliders, fix slider nerf being too lenient
* Move cubing to performance calculation
* Add separate list for slider strains
* Rename difficulty atttribute
* Rename attribute in perfcalc
* Check if AimDifficultSliderCount is more than 0, code quality fixes
* Add `AimDifficultSliderCount` to the list of databased attributes
* Code quality
---------
Co-authored-by: James Wilson
---
.../Difficulty/OsuDifficultyAttributes.cs | 9 ++++
.../Difficulty/OsuDifficultyCalculator.cs | 3 +-
.../Difficulty/OsuPerformanceCalculator.cs | 49 +++++++++----------
.../Difficulty/Skills/Aim.cs | 24 +++++++++
.../Difficulty/DifficultyAttributes.cs | 1 +
5 files changed, 60 insertions(+), 26 deletions(-)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
index a3c0209a08..3b9a23df23 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
@@ -8,6 +8,7 @@ using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty
{
@@ -19,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("aim_difficulty")]
public double AimDifficulty { get; set; }
+ ///
+ /// The number of s weighted by difficulty.
+ ///
+ [JsonProperty("aim_difficult_slider_count")]
+ public double AimDifficultSliderCount { get; set; }
+
///
/// The difficulty corresponding to the speed skill.
///
@@ -109,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount);
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
+ yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
}
public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo)
@@ -125,6 +133,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
+ AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
DrainRate = onlineInfo.DrainRate;
HitCircleCount = onlineInfo.CircleCount;
SliderCount = onlineInfo.SliderCount;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 575e03051c..ffdd4673e3 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
double speedNotes = ((Speed)skills[2]).RelevantNoteCount();
-
+ double difficultSliders = ((Aim)skills[0]).GetDifficultSliders();
double flashlightRating = 0.0;
if (mods.Any(h => h is OsuModFlashlight))
@@ -99,6 +99,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
StarRating = starRating,
Mods = mods,
AimDifficulty = aimRating,
+ AimDifficultSliderCount = difficultSliders,
SpeedDifficulty = speedRating,
SpeedNoteCount = speedNotes,
FlashlightDifficulty = flashlightRating,
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 31b00dba2b..3610845533 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -135,7 +135,30 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
- double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
+ double aimDifficulty = attributes.AimDifficulty;
+
+ if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0)
+ {
+ double estimateImproperlyFollowedDifficultSliders;
+
+ if (usingClassicSliderAccuracy)
+ {
+ // When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
+ int maximumPossibleDroppedSliders = totalImperfectHits;
+ estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, attributes.AimDifficultSliderCount);
+ }
+ else
+ {
+ // We add tick misses here since they too mean that the player didn't follow the slider properly
+ // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
+ estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, attributes.AimDifficultSliderCount);
+ }
+
+ double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / attributes.AimDifficultSliderCount, 3) + attributes.SliderFactor;
+ aimDifficulty *= sliderNerfFactor;
+ }
+
+ double aimValue = OsuStrainSkill.DifficultyToPerformance(aimDifficulty);
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@@ -163,30 +186,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
}
- // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.
- double estimateDifficultSliders = attributes.SliderCount * 0.15;
-
- if (attributes.SliderCount > 0)
- {
- double estimateImproperlyFollowedDifficultSliders;
-
- if (usingClassicSliderAccuracy)
- {
- // When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
- int maximumPossibleDroppedSliders = totalImperfectHits;
- estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
- }
- else
- {
- // We add tick misses here since they too mean that the player didn't follow the slider properly
- // We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
- estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, estimateDifficultSliders);
- }
-
- double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor;
- aimValue *= sliderNerfFactor;
- }
-
aimValue *= accuracy;
// It is important to consider accuracy difficulty when scaling with accuracy.
aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index faf91e4652..400bc97fbc 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -2,9 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
+using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
+using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
@@ -26,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double skillMultiplier => 25.18;
private double strainDecayBase => 0.15;
+ private readonly List sliderStrains = new List();
+
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime);
@@ -35,7 +40,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
currentStrain *= strainDecay(current.DeltaTime);
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier;
+ if (current.BaseObject is Slider)
+ {
+ sliderStrains.Add(currentStrain);
+ }
+
return currentStrain;
}
+
+ public double GetDifficultSliders()
+ {
+ if (sliderStrains.Count == 0)
+ return 0;
+
+ double[] sortedStrains = sliderStrains.OrderDescending().ToArray();
+
+ double maxSliderStrain = sortedStrains.Max();
+ if (maxSliderStrain == 0)
+ return 0;
+
+ return sortedStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0))));
+ }
}
}
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
index 7b6bc37a61..f5ed5a180b 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
@@ -30,6 +30,7 @@ namespace osu.Game.Rulesets.Difficulty
protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 25;
protected const int ATTRIB_ID_OK_HIT_WINDOW = 27;
protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29;
+ protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31;
///
/// The mods which were applied to the beatmap.
From 3ddeaf8460476e8e8c8386e584addd8f9594d0d1 Mon Sep 17 00:00:00 2001
From: James Wilson
Date: Tue, 24 Dec 2024 09:43:44 +0000
Subject: [PATCH 009/262] Use `lastAngle` when nerfing repeated angles on acute
bonus (#31245)
* Use `lastAngle` when nerfing repeated angles on acute bonus
* Bump acute multiplier
* Correct outdated wiggle bonus comment
* Update test
---------
Co-authored-by: StanR
---
.../OsuDifficultyCalculatorTest.cs | 2 +-
.../Difficulty/Evaluators/AimEvaluator.cs | 11 +++++------
2 files changed, 6 insertions(+), 7 deletions(-)
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index 9798611488..c0a6d3a755 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(9.4310274277499619d, 239, "diffcalc-test")]
+ [TestCase(9.6343245007055653d, 239, "diffcalc-test")]
[TestCase(1.7550169162648608d, 54, "zero-length-sliders")]
[TestCase(0.55231632896800109d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index c3270f25f8..fdf94719ed 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
public static class AimEvaluator
{
private const double wide_angle_multiplier = 1.5;
- private const double acute_angle_multiplier = 2.35;
+ private const double acute_angle_multiplier = 2.7;
private const double slider_multiplier = 1.35;
private const double velocity_change_multiplier = 0.75;
private const double wiggle_multiplier = 1.02;
@@ -75,7 +75,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{
double currAngle = osuCurrObj.Angle.Value;
double lastAngle = osuLastObj.Angle.Value;
- double lastLastAngle = osuLastLastObj.Angle.Value;
// Rewarding angles, take the smaller velocity as base.
double angleBonus = Math.Min(currVelocity, prevVelocity);
@@ -90,11 +89,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)));
- // Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse.
- acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3)));
+ // Penalize acute angles if they're repeated, reducing the penalty as the lastAngle gets more obtuse.
+ acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
- // Apply wiggle bonus for jumps that are [radius, 2*diameter] in distance, with < 110 angle and bpm > 150
- // https://www.desmos.com/calculator/iis7lgbppe
+ // Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
+ // https://www.desmos.com/calculator/dp0v0nvowc
wiggleBonus = angleBonus
* DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter)
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
From 824497d82c6f86eebf6421b1cdcf25beaf39f881 Mon Sep 17 00:00:00 2001
From: Jay Lawton
Date: Fri, 27 Dec 2024 23:30:30 +1000
Subject: [PATCH 010/262] Rewrite of the `Rhythm` Skill within osu!taiko
(#31284)
* implement bell curve into diffcalcutils
* remove unneeded attributes
* implement new rhythm skill
* change dho variables
* update dho rhythm
* interval interface
* implement rhythmevaluator
* evenhitobjects
* evenpatterns
* evenrhythm
* change attribute ordering
* initial balancing
* change naming to Same instead of Even
* remove attribute bump for display
* Fix diffcalc tests
---------
Co-authored-by: StanR
---
.../TaikoDifficultyCalculatorTest.cs | 8 +-
.../Difficulty/Evaluators/RhythmEvaluator.cs | 149 +++++++++++++++++
.../Preprocessing/Rhythm/Data/SamePatterns.cs | 55 ++++++
.../Preprocessing/Rhythm/Data/SameRhythm.cs | 73 ++++++++
.../Rhythm/Data/SameRhythmHitObjects.cs | 94 +++++++++++
.../Preprocessing/Rhythm/IHasInterval.cs | 13 ++
.../Rhythm/TaikoDifficultyHitObjectRhythm.cs | 79 ++++++++-
.../Preprocessing/TaikoDifficultyHitObject.cs | 51 ++----
.../Difficulty/Skills/Rhythm.cs | 157 ++----------------
.../Difficulty/TaikoDifficultyAttributes.cs | 28 ++--
.../Difficulty/TaikoDifficultyCalculator.cs | 20 ++-
.../Utils/DifficultyCalculationUtils.cs | 10 ++
12 files changed, 520 insertions(+), 217 deletions(-)
create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs
create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs
create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs
create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index 09d6540f72..ba247c68d4 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(3.0920212594351191d, 200, "diffcalc-test")]
- [TestCase(3.0920212594351191d, 200, "diffcalc-test-strong")]
+ [TestCase(3.0950934814938953d, 200, "diffcalc-test")]
+ [TestCase(3.0950934814938953d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(4.0789820318081444d, 200, "diffcalc-test")]
- [TestCase(4.0789820318081444d, 200, "diffcalc-test-strong")]
+ [TestCase(4.0839365008715403d, 200, "diffcalc-test")]
+ [TestCase(4.0839365008715403d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
new file mode 100644
index 0000000000..3a294f7123
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
@@ -0,0 +1,149 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
+{
+ public class RhythmEvaluator
+ {
+ ///
+ /// Multiplier for a given denominator term.
+ ///
+ private static double termPenalty(double ratio, int denominator, double power, double multiplier)
+ {
+ return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power);
+ }
+
+ ///
+ /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses.
+ ///
+ private static double ratioDifficulty(double ratio, int terms = 8)
+ {
+ double difficulty = 0;
+
+ for (int i = 1; i <= terms; ++i)
+ {
+ difficulty += termPenalty(ratio, i, 2, 1);
+ }
+
+ difficulty += terms;
+
+ // Give bonus to near-1 ratios
+ difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.7);
+
+ // Penalize ratios that are VERY near 1
+ difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5);
+
+ return difficulty / Math.Sqrt(8);
+ }
+
+ ///
+ /// Determines if the changes in hit object intervals is consistent based on a given threshold.
+ ///
+ private static double repeatedIntervalPenalty(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow, double threshold = 0.1)
+ {
+ double longIntervalPenalty = sameInterval(sameRhythmHitObjects, 3);
+
+ double shortIntervalPenalty = sameRhythmHitObjects.Children.Count < 6
+ ? sameInterval(sameRhythmHitObjects, 4)
+ : 1.0; // Returns a non-penalty if there are 6 or more notes within an interval.
+
+ // Scale penalties dynamically based on hit object duration relative to hitWindow.
+ double penaltyScaling = Math.Max(1 - sameRhythmHitObjects.Duration / (hitWindow * 2), 0.5);
+
+ return Math.Min(longIntervalPenalty, shortIntervalPenalty) * penaltyScaling;
+
+ double sameInterval(SameRhythmHitObjects startObject, int intervalCount)
+ {
+ List intervals = new List();
+ var currentObject = startObject;
+
+ for (int i = 0; i < intervalCount && currentObject != null; i++)
+ {
+ intervals.Add(currentObject.HitObjectInterval);
+ currentObject = currentObject.Previous;
+ }
+
+ intervals.RemoveAll(interval => interval == null);
+
+ if (intervals.Count < intervalCount)
+ return 1.0; // No penalty if there aren't enough valid intervals.
+
+ for (int i = 0; i < intervals.Count; i++)
+ {
+ for (int j = i + 1; j < intervals.Count; j++)
+ {
+ double ratio = intervals[i]!.Value / intervals[j]!.Value;
+ if (Math.Abs(1 - ratio) <= threshold) // If any two intervals are similar, apply a penalty.
+ return 0.3;
+ }
+ }
+
+ return 1.0; // No penalty if all intervals are different.
+ }
+ }
+
+ private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow)
+ {
+ double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio);
+ double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval;
+
+ // If a previous interval exists and there are multiple hit objects in the sequence:
+ if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1)
+ {
+ double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count;
+ double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious;
+
+ if (durationDifference > 0)
+ {
+ intervalDifficulty *= DifficultyCalculationUtils.Logistic(
+ durationDifference / hitWindow,
+ midpointOffset: 0.7,
+ multiplier: 1.5,
+ maxValue: 1);
+ }
+ }
+
+ // Apply consistency penalty.
+ intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow);
+
+ // Penalise patterns that can be hit within a single hit window.
+ intervalDifficulty *= DifficultyCalculationUtils.Logistic(
+ sameRhythmHitObjects.Duration / hitWindow,
+ midpointOffset: 0.6,
+ multiplier: 1,
+ maxValue: 1);
+
+ return Math.Pow(intervalDifficulty, 0.75);
+ }
+
+ private static double evaluateDifficultyOf(SamePatterns samePatterns)
+ {
+ return ratioDifficulty(samePatterns.IntervalRatio);
+ }
+
+ ///
+ /// Evaluate the difficulty of a hitobject considering its interval change.
+ ///
+ public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow)
+ {
+ TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm;
+ double difficulty = 0.0d;
+
+ if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects
+ difficulty += evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow);
+
+ if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns
+ difficulty += 0.5 * evaluateDifficultyOf(rhythm.SamePatterns);
+
+ return difficulty;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs
new file mode 100644
index 0000000000..50839c4561
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs
@@ -0,0 +1,55 @@
+// 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;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
+{
+ ///
+ /// Represents grouped by their 's interval.
+ ///
+ public class SamePatterns : SameRhythm
+ {
+ public SamePatterns? Previous { get; private set; }
+
+ ///
+ /// The between children within this group.
+ /// If there is only one child, this will have the value of the first child's .
+ ///
+ public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval;
+
+ ///
+ /// The ratio of between this and the previous . In the
+ /// case where there is no previous , this will have a value of 1.
+ ///
+ public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d;
+
+ public TaikoDifficultyHitObject FirstHitObject => Children[0].FirstHitObject;
+
+ public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children);
+
+ private SamePatterns(SamePatterns? previous, List data, ref int i)
+ : base(data, ref i, 5)
+ {
+ Previous = previous;
+
+ foreach (TaikoDifficultyHitObject hitObject in AllHitObjects)
+ {
+ hitObject.Rhythm.SamePatterns = this;
+ }
+ }
+
+ public static void GroupPatterns(List data)
+ {
+ List samePatterns = new List();
+
+ // Index does not need to be incremented, as it is handled within the SameRhythm constructor.
+ for (int i = 0; i < data.Count;)
+ {
+ SamePatterns? previous = samePatterns.Count > 0 ? samePatterns[^1] : null;
+ samePatterns.Add(new SamePatterns(previous, data, ref i));
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs
new file mode 100644
index 0000000000..b1ca22595b
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs
@@ -0,0 +1,73 @@
+// 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.Rhythm.Data
+{
+ ///
+ /// A base class for grouping s by their interval. In edges where an interval change
+ /// occurs, the is added to the group with the smaller interval.
+ ///
+ public abstract class SameRhythm
+ where ChildType : IHasInterval
+ {
+ public IReadOnlyList Children { get; private set; }
+
+ ///
+ /// Determines if the intervals between two child objects are within a specified margin of error,
+ /// indicating that the intervals are effectively "flat" or consistent.
+ ///
+ private bool isFlat(ChildType current, ChildType previous, double marginOfError)
+ {
+ return Math.Abs(current.Interval - previous.Interval) <= marginOfError;
+ }
+
+ ///
+ /// Create a new from a list of s, and add
+ /// them to the list until the end of the group.
+ ///
+ /// The list of s.
+ ///
+ /// Index in to start adding children. This will be modified and should be passed into
+ /// the next 's constructor.
+ ///
+ ///
+ /// The margin of error for the interval, within of which no interval change is considered to have occured.
+ ///
+ protected SameRhythm(List data, ref int i, double marginOfError)
+ {
+ List children = new List();
+ Children = children;
+ children.Add(data[i]);
+ i++;
+
+ for (; i < data.Count - 1; i++)
+ {
+ // An interval change occured, add the current data if the next interval is larger.
+ if (!isFlat(data[i], data[i + 1], marginOfError))
+ {
+ if (data[i + 1].Interval > data[i].Interval + marginOfError)
+ {
+ children.Add(data[i]);
+ i++;
+ }
+
+ return;
+ }
+
+ // No interval change occured
+ children.Add(data[i]);
+ }
+
+ // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error.
+ // If true, add the current object to the group and increment the index to process the next object.
+ if (data.Count > 2 && isFlat(data[^1], data[^2], marginOfError))
+ {
+ children.Add(data[i]);
+ i++;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs
new file mode 100644
index 0000000000..0ccc6da026
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs
@@ -0,0 +1,94 @@
+// 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;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
+{
+ ///
+ /// Represents a group of s with no rhythm variation.
+ ///
+ public class SameRhythmHitObjects : SameRhythm, IHasInterval
+ {
+ public TaikoDifficultyHitObject FirstHitObject => Children[0];
+
+ public SameRhythmHitObjects? Previous;
+
+ ///
+ /// of the first hit object.
+ ///
+ public double StartTime => Children[0].StartTime;
+
+ ///
+ /// The interval between the first and final hit object within this group.
+ ///
+ public double Duration => Children[^1].StartTime - Children[0].StartTime;
+
+ ///
+ /// The interval in ms of each hit object in this . This is only defined if there is
+ /// more than two hit objects in this .
+ ///
+ public double? HitObjectInterval;
+
+ ///
+ /// The ratio of between this and the previous . In the
+ /// case where one or both of the is undefined, this will have a value of 1.
+ ///
+ public double HitObjectIntervalRatio = 1;
+
+ ///
+ /// The interval between the of this and the previous .
+ ///
+ public double Interval { get; private set; } = double.PositiveInfinity;
+
+ public SameRhythmHitObjects(SameRhythmHitObjects? previous, List data, ref int i)
+ : base(data, ref i, 5)
+ {
+ Previous = previous;
+
+ foreach (var hitObject in Children)
+ {
+ hitObject.Rhythm.SameRhythmHitObjects = this;
+
+ // Pass the HitObjectInterval to each child.
+ hitObject.HitObjectInterval = HitObjectInterval;
+ }
+
+ calculateIntervals();
+ }
+
+ public static List GroupHitObjects(List data)
+ {
+ List flatPatterns = new List();
+
+ // Index does not need to be incremented, as it is handled within SameRhythm's constructor.
+ for (int i = 0; i < data.Count;)
+ {
+ SameRhythmHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null;
+ flatPatterns.Add(new SameRhythmHitObjects(previous, data, ref i));
+ }
+
+ return flatPatterns;
+ }
+
+ private void calculateIntervals()
+ {
+ // Calculate the average interval between hitobjects, or null if there are fewer than two.
+ HitObjectInterval = Children.Count < 2 ? null : (Children[^1].StartTime - Children[0].StartTime) / (Children.Count - 1);
+
+ // If both the current and previous intervals are available, calculate the ratio.
+ if (Previous?.HitObjectInterval != null && HitObjectInterval != null)
+ {
+ HitObjectIntervalRatio = HitObjectInterval.Value / Previous.HitObjectInterval.Value;
+ }
+
+ if (Previous == null)
+ {
+ return;
+ }
+
+ Interval = StartTime - Previous.StartTime;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs
new file mode 100644
index 0000000000..8f3917cbde
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
+{
+ ///
+ /// The interface for hitobjects that provide an interval value.
+ ///
+ public interface IHasInterval
+ {
+ double Interval { get; }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs
index a273d7e2ea..beb7bfe5f6 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs
@@ -1,35 +1,98 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Linq;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
+
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
{
///
- /// Represents a rhythm change in a taiko map.
+ /// Stores rhythm data for a .
///
public class TaikoDifficultyHitObjectRhythm
{
///
- /// The difficulty multiplier associated with this rhythm change.
+ /// The group of hit objects with consistent rhythm that this object belongs to.
///
- public readonly double Difficulty;
+ public SameRhythmHitObjects? SameRhythmHitObjects;
///
- /// The ratio of current
- /// to previous for the rhythm change.
+ /// The larger pattern of rhythm groups that this object is part of.
+ ///
+ public SamePatterns? SamePatterns;
+
+ ///
+ /// The ratio of current
+ /// to previous for the rhythm change.
/// A above 1 indicates a slow-down; a below 1 indicates a speed-up.
///
public readonly double Ratio;
+ ///
+ /// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object.
+ ///
+ ///
+ /// The general guidelines for the values are:
+ ///
+ /// - rhythm changes with ratio closer to 1 (that are not 1) are harder to play,
+ /// - speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch).
+ ///
+ ///
+ private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms =
+ {
+ new TaikoDifficultyHitObjectRhythm(1, 1),
+ new TaikoDifficultyHitObjectRhythm(2, 1),
+ new TaikoDifficultyHitObjectRhythm(1, 2),
+ new TaikoDifficultyHitObjectRhythm(3, 1),
+ new TaikoDifficultyHitObjectRhythm(1, 3),
+ new TaikoDifficultyHitObjectRhythm(3, 2),
+ new TaikoDifficultyHitObjectRhythm(2, 3),
+ new TaikoDifficultyHitObjectRhythm(5, 4),
+ new TaikoDifficultyHitObjectRhythm(4, 5)
+ };
+
+ ///
+ /// Initialises a new instance of s,
+ /// calculating the closest rhythm change and its associated difficulty for the current hit object.
+ ///
+ /// The current being processed.
+ public TaikoDifficultyHitObjectRhythm(TaikoDifficultyHitObject current)
+ {
+ var previous = current.Previous(0);
+
+ if (previous == null)
+ {
+ Ratio = 1;
+ return;
+ }
+
+ TaikoDifficultyHitObjectRhythm closestRhythm = getClosestRhythm(current.DeltaTime, previous.DeltaTime);
+ Ratio = closestRhythm.Ratio;
+ }
+
///
/// Creates an object representing a rhythm change.
///
/// The numerator for .
/// The denominator for
- /// The difficulty multiplier associated with this rhythm change.
- public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty)
+ private TaikoDifficultyHitObjectRhythm(int numerator, int denominator)
{
Ratio = numerator / (double)denominator;
- Difficulty = difficulty;
+ }
+
+ ///
+ /// Determines the closest rhythm change from that matches the timing ratio
+ /// between the current and previous intervals.
+ ///
+ /// The time difference between the current hit object and the previous one.
+ /// The time difference between the previous hit object and the one before it.
+ /// The closest matching rhythm from .
+ private TaikoDifficultyHitObjectRhythm getClosestRhythm(double currentDeltaTime, double previousDeltaTime)
+ {
+ double ratio = currentDeltaTime / previousDeltaTime;
+ return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
}
}
}
+
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
index e741e4c9e7..dfcd08ed94 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.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.
-using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
@@ -15,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
///
/// Represents a single hit object in taiko difficulty calculation.
///
- public class TaikoDifficultyHitObject : DifficultyHitObject
+ public class TaikoDifficultyHitObject : DifficultyHitObject, IHasInterval
{
///
/// The list of all of the same colour as this in the beatmap.
@@ -42,6 +41,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
///
public readonly TaikoDifficultyHitObjectRhythm Rhythm;
+ ///
+ /// The interval between this hit object and the surrounding hit objects in its rhythm group.
+ ///
+ public double? HitObjectInterval { get; set; }
+
///
/// 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.
@@ -58,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
///
public double CurrentSliderVelocity;
+ public double Interval => DeltaTime;
+
///
/// Creates a new difficulty hit object.
///
@@ -81,7 +87,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
// Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor
Colour = new TaikoDifficultyHitObjectColour();
- Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate);
+
+ // Create a Rhythm object, its properties are filled in by TaikoDifficultyHitObjectRhythm
+ Rhythm = new TaikoDifficultyHitObjectRhythm(this);
switch ((hitObject as Hit)?.Type)
{
@@ -105,43 +113,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
}
}
- ///
- /// List of most common rhythm changes in taiko maps.
- ///
- ///
- /// The general guidelines for the values are:
- ///
- /// - rhythm changes with ratio closer to 1 (that are not 1) are harder to play,
- /// - speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch).
- ///
- ///
- private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms =
- {
- new TaikoDifficultyHitObjectRhythm(1, 1, 0.0),
- new TaikoDifficultyHitObjectRhythm(2, 1, 0.3),
- new TaikoDifficultyHitObjectRhythm(1, 2, 0.5),
- new TaikoDifficultyHitObjectRhythm(3, 1, 0.3),
- new TaikoDifficultyHitObjectRhythm(1, 3, 0.35),
- new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), // purposefully higher (requires hand switch in full alternating gameplay style)
- new TaikoDifficultyHitObjectRhythm(2, 3, 0.4),
- new TaikoDifficultyHitObjectRhythm(5, 4, 0.5),
- new TaikoDifficultyHitObjectRhythm(4, 5, 0.7)
- };
-
- ///
- /// Returns the closest rhythm change from required to hit this object.
- ///
- /// The gameplay preceding this one.
- /// The gameplay preceding .
- /// The rate of the gameplay clock.
- private TaikoDifficultyHitObjectRhythm getClosestRhythm(HitObject lastObject, HitObject lastLastObject, double clockRate)
- {
- double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate;
- double ratio = DeltaTime / prevLength;
-
- return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
- }
-
public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1));
public TaikoDifficultyHitObject? NextMono(int forwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex + (forwardsIndex + 1));
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
index e76af13686..4fe1ea693e 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
@@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using osu.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.Objects;
-using osu.Game.Utils;
+using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
@@ -16,158 +14,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
public class Rhythm : StrainDecaySkill
{
- protected override double SkillMultiplier => 10;
- protected override double StrainDecayBase => 0;
+ protected override double SkillMultiplier => 1.0;
+ protected override double StrainDecayBase => 0.4;
- ///
- /// The note-based decay for rhythm strain.
- ///
- ///
- /// is not used here, as it's time- and not note-based.
- ///
- private const double strain_decay = 0.96;
+ private readonly double greatHitWindow;
- ///
- /// Maximum number of entries in .
- ///
- private const int rhythm_history_max_length = 8;
-
- ///
- /// Contains the last changes in note sequence rhythms.
- ///
- private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length);
-
- ///
- /// Contains the rolling rhythm strain.
- /// Used to apply per-note decay.
- ///
- private double currentStrain;
-
- ///
- /// Number of notes since the last rhythm change has taken place.
- ///
- private int notesSinceRhythmChange;
-
- public Rhythm(Mod[] mods)
+ public Rhythm(Mod[] mods, double greatHitWindow)
: base(mods)
{
+ this.greatHitWindow = greatHitWindow;
}
protected override double StrainValueOf(DifficultyHitObject current)
{
- // drum rolls and swells are exempt.
- if (!(current.BaseObject is Hit))
- {
- resetRhythmAndStrain();
- return 0.0;
- }
+ double difficulty = RhythmEvaluator.EvaluateDifficultyOf(current, greatHitWindow);
- currentStrain *= strain_decay;
+ // To prevent abuse of exceedingly long intervals between awkward rhythms, we penalise its difficulty.
+ difficulty *= DifficultyCalculationUtils.Logistic(current.DeltaTime, 350, -1 / 25.0, 0.5) + 0.5;
- TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
- notesSinceRhythmChange += 1;
-
- // rhythm difficulty zero (due to rhythm not changing) => no rhythm strain.
- if (hitObject.Rhythm.Difficulty == 0.0)
- {
- return 0.0;
- }
-
- double objectStrain = hitObject.Rhythm.Difficulty;
-
- objectStrain *= repetitionPenalties(hitObject);
- objectStrain *= patternLengthPenalty(notesSinceRhythmChange);
- objectStrain *= speedPenalty(hitObject.DeltaTime);
-
- // careful - needs to be done here since calls above read this value
- notesSinceRhythmChange = 0;
-
- currentStrain += objectStrain;
- return currentStrain;
- }
-
- ///
- /// Returns a penalty to apply to the current hit object caused by repeating rhythm changes.
- ///
- ///
- /// Repetitions of more recent patterns are associated with a higher penalty.
- ///
- /// The current hit object being considered.
- private double repetitionPenalties(TaikoDifficultyHitObject hitObject)
- {
- double penalty = 1;
-
- rhythmHistory.Enqueue(hitObject);
-
- for (int mostRecentPatternsToCompare = 2; mostRecentPatternsToCompare <= rhythm_history_max_length / 2; mostRecentPatternsToCompare++)
- {
- for (int start = rhythmHistory.Count - mostRecentPatternsToCompare - 1; start >= 0; start--)
- {
- if (!samePattern(start, mostRecentPatternsToCompare))
- continue;
-
- int notesSince = hitObject.Index - rhythmHistory[start].Index;
- penalty *= repetitionPenalty(notesSince);
- break;
- }
- }
-
- return penalty;
- }
-
- ///
- /// Determines whether the rhythm change pattern starting at is a repeat of any of the
- /// .
- ///
- private bool samePattern(int start, int mostRecentPatternsToCompare)
- {
- for (int i = 0; i < mostRecentPatternsToCompare; i++)
- {
- if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - mostRecentPatternsToCompare + i].Rhythm)
- return false;
- }
-
- return true;
- }
-
- ///
- /// Calculates a single rhythm repetition penalty.
- ///
- /// Number of notes since the last repetition of a rhythm change.
- private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
-
- ///
- /// Calculates a penalty based on the number of notes since the last rhythm change.
- /// Both rare and frequent rhythm changes are penalised.
- ///
- /// Number of notes since the last rhythm change.
- private static double patternLengthPenalty(int patternLength)
- {
- double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0);
- double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0);
- return Math.Min(shortPatternPenalty, longPatternPenalty);
- }
-
- ///
- /// Calculates a penalty for objects that do not require alternating hands.
- ///
- /// Time (in milliseconds) since the last hit object.
- private double speedPenalty(double deltaTime)
- {
- if (deltaTime < 80) return 1;
- if (deltaTime < 210) return Math.Max(0, 1.4 - 0.005 * deltaTime);
-
- resetRhythmAndStrain();
- return 0.0;
- }
-
- ///
- /// Resets the rolling strain value and counter.
- ///
- private void resetRhythmAndStrain()
- {
- currentStrain = 0.0;
- notesSinceRhythmChange = 0;
+ return difficulty;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
index d3cdb379d5..ef729e1f07 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
@@ -10,18 +10,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoDifficultyAttributes : DifficultyAttributes
{
- ///
- /// The difficulty corresponding to the stamina skill.
- ///
- [JsonProperty("stamina_difficulty")]
- public double StaminaDifficulty { get; set; }
-
- ///
- /// The ratio of stamina difficulty from mono-color (single colour) streams to total stamina difficulty.
- ///
- [JsonProperty("mono_stamina_factor")]
- public double MonoStaminaFactor { get; set; }
-
///
/// The difficulty corresponding to the rhythm skill.
///
@@ -40,8 +28,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
[JsonProperty("colour_difficulty")]
public double ColourDifficulty { get; set; }
- [JsonProperty("rhythm_difficult_strains")]
- public double RhythmTopStrains { get; set; }
+ ///
+ /// The difficulty corresponding to the stamina skill.
+ ///
+ [JsonProperty("stamina_difficulty")]
+ public double StaminaDifficulty { get; set; }
+
+ ///
+ /// The ratio of stamina difficulty from mono-color (single colour) streams to total stamina difficulty.
+ ///
+ [JsonProperty("mono_stamina_factor")]
+ public double MonoStaminaFactor { get; set; }
+
+ [JsonProperty("reading_difficult_strains")]
+ public double ReadingTopStrains { get; set; }
[JsonProperty("colour_difficult_strains")]
public double ColourTopStrains { get; set; }
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index 0d6ecb8d3e..f8ff6f6065 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -14,6 +14,7 @@ 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.Preprocessing.Reading;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Scoring;
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
public class TaikoDifficultyCalculator : DifficultyCalculator
{
private const double difficulty_multiplier = 0.084375;
- private const double rhythm_skill_multiplier = 0.200 * difficulty_multiplier;
+ private const double rhythm_skill_multiplier = 1.24 * difficulty_multiplier;
private const double reading_skill_multiplier = 0.100 * difficulty_multiplier;
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier;
@@ -37,9 +38,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{
+ HitWindows hitWindows = new HitWindows();
+ hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
+
return new Skill[]
{
- new Rhythm(mods),
+ new Rhythm(mods, hitWindows.WindowFor(HitResult.Great) / clockRate),
new Reading(mods),
new Colour(mods),
new Stamina(mods, false),
@@ -57,6 +61,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
+ var hitWindows = new HitWindows();
+ hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
+
var difficultyHitObjects = new List();
var centreObjects = new List();
var rimObjects = new List();
@@ -79,7 +86,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
));
}
+ var groupedHitObjects = SameRhythmHitObjects.GroupHitObjects(noteObjects);
+
TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects);
+ SamePatterns.GroupPatterns(groupedHitObjects);
bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate);
return difficultyHitObjects;
@@ -105,8 +115,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier;
double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5);
- double rhythmDifficultStrains = rhythm.CountTopWeightedStrains();
double colourDifficultStrains = colour.CountTopWeightedStrains();
+ double readingDifficultStrains = reading.CountTopWeightedStrains();
double staminaDifficultStrains = stamina.CountTopWeightedStrains();
double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax);
@@ -134,9 +144,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
ColourDifficulty = colourRating,
StaminaDifficulty = staminaRating,
MonoStaminaFactor = monoStaminaFactor,
- StaminaTopStrains = staminaDifficultStrains,
- RhythmTopStrains = rhythmDifficultStrains,
+ ReadingTopStrains = readingDifficultStrains,
ColourTopStrains = colourDifficultStrains,
+ StaminaTopStrains = staminaDifficultStrains,
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
MaxCombo = beatmap.GetMaxCombo(),
diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
index 055d8a458b..497a1f8234 100644
--- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
+++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
@@ -56,6 +56,16 @@ namespace osu.Game.Rulesets.Difficulty.Utils
/// The p-norm of the vector.
public static double Norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
+ ///
+ /// Calculates a Gaussian-based bell curve function (https://en.wikipedia.org/wiki/Gaussian_function)
+ ///
+ /// Value to calculate the function for
+ /// The mean (center) of the bell curve
+ /// The width (spread) of the curve
+ /// Multiplier to adjust the curve's height
+ /// The output of the bell curve function of
+ public static double BellCurve(double x, double mean, double width, double multiplier = 1.0) => multiplier * Math.Exp(Math.E * -(Math.Pow(x - mean, 2) / Math.Pow(width, 2)));
+
///
/// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations)
///
From 988ed374ae82528991f37516ee40098d2adf1af4 Mon Sep 17 00:00:00 2001
From: James Wilson
Date: Sun, 29 Dec 2024 19:29:57 +0000
Subject: [PATCH 011/262] Add basic difficulty & performance calculation for
Autopilot mod on osu! ruleset (#21211)
* Set speed distance to 0
* Reduce speed & flashlight, remove aim
* Remove speed AR bonus
* cleanup autopilot mod check in `SpeedEvaluator`
* further decrease speed rating for extra hand availability
* Pass all mods to the speed evaluator, zero out distance bonus instead of distance
---------
Co-authored-by: tsunyoku
Co-authored-by: StanR
---
.../Difficulty/Evaluators/SpeedEvaluator.cs | 9 ++++++++-
.../Difficulty/OsuDifficultyCalculator.cs | 6 ++++++
.../Difficulty/OsuPerformanceCalculator.cs | 6 ++++++
osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +-
4 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs
index a5f6468f17..e5e9769081 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs
@@ -2,9 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
+using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
@@ -24,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
/// - and how easily they can be cheesed.
///
///
- public static double EvaluateDifficultyOf(DifficultyHitObject current)
+ public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnlyList mods)
{
if (current.BaseObject is Spinner)
return 0;
@@ -56,6 +60,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
+ if (mods.OfType().Any())
+ distanceBonus = 0;
+
// Base difficulty with all bonuses
double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index ffdd4673e3..d0f23735c3 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -63,6 +63,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedRating = 0.0;
flashlightRating *= 0.7;
}
+ else if (mods.Any(h => h is OsuModAutopilot))
+ {
+ speedRating *= 0.5;
+ aimRating = 0.0;
+ flashlightRating *= 0.4;
+ }
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 3610845533..df418fb3f8 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -135,6 +135,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
+ if (score.Mods.Any(h => h is OsuModAutopilot))
+ return 0.0;
+
double aimDifficulty = attributes.AimDifficulty;
if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0)
@@ -211,6 +214,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
+ if (score.Mods.Any(h => h is OsuModAutopilot))
+ approachRateFactor = 0.0;
+
speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
if (score.Mods.Any(m => m is OsuModBlinds))
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index d2c4bbb618..5dae9a9fc5 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override double StrainValueAt(DifficultyHitObject current)
{
currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime);
- currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
+ currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, Mods) * skillMultiplier;
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
From 76ac11ff593bafc32a99a92368f79c94dac2f512 Mon Sep 17 00:00:00 2001
From: StanR
Date: Mon, 6 Jan 2025 20:08:14 +0500
Subject: [PATCH 012/262] Fix angle bonuses calculating repetition incorrectly,
apply distance scaling to wide bonus (#31320)
* Fix angle bonuses calculating repetition incorrectly, apply distance scaling to wide bonus
* Buff speed to compensate for streams losing pp
* Adjust speed multiplier
* Adjust wide scaling
* Fix tests
---
.../OsuDifficultyCalculatorTest.cs | 18 ++++++++---------
.../Difficulty/Evaluators/AimEvaluator.cs | 20 ++++++++++---------
.../Difficulty/Evaluators/SpeedEvaluator.cs | 2 +-
.../Difficulty/Skills/Speed.cs | 2 +-
4 files changed, 22 insertions(+), 20 deletions(-)
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index c0a6d3a755..842a34aaa8 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
- [TestCase(6.718709884850683d, 239, "diffcalc-test")]
- [TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
- [TestCase(0.42630400627180914d, 4, "very-fast-slider")]
+ [TestCase(6.7153612142198682d, 239, "diffcalc-test")]
+ [TestCase(1.4484916289194889d, 54, "zero-length-sliders")]
+ [TestCase(0.42912495021837549d, 4, "very-fast-slider")]
[TestCase(0.14143808967817237d, 2, "nan-slider")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(9.6343245007055653d, 239, "diffcalc-test")]
- [TestCase(1.7550169162648608d, 54, "zero-length-sliders")]
- [TestCase(0.55231632896800109d, 4, "very-fast-slider")]
+ [TestCase(9.6358837846598835d, 239, "diffcalc-test")]
+ [TestCase(1.754888327422514d, 54, "zero-length-sliders")]
+ [TestCase(0.55601568006454294d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
- [TestCase(6.718709884850683d, 239, "diffcalc-test")]
- [TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
- [TestCase(0.42630400627180914d, 4, "very-fast-slider")]
+ [TestCase(6.7153612142198682d, 239, "diffcalc-test")]
+ [TestCase(1.4484916289194889d, 54, "zero-length-sliders")]
+ [TestCase(0.42912495021837549d, 4, "very-fast-slider")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index fdf94719ed..cff2eae357 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -80,17 +80,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double angleBonus = Math.Min(currVelocity, prevVelocity);
wideAngleBonus = calcWideAngleBonus(currAngle);
+ acuteAngleBonus = calcAcuteAngleBonus(currAngle);
+
+ // Penalize angle repetition.
+ wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
+ acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
+
+ // Apply full wide angle bonus for distance more than one diameter
+ wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
- acuteAngleBonus = calcAcuteAngleBonus(currAngle) *
- angleBonus *
- DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) *
- DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
-
- // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
- wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)));
- // Penalize acute angles if they're repeated, reducing the penalty as the lastAngle gets more obtuse.
- acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
+ acuteAngleBonus *= angleBonus *
+ DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) *
+ DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
// https://www.desmos.com/calculator/dp0v0nvowc
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs
index e5e9769081..769220ece0 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
private const double min_speed_bonus = 200; // 200 BPM 1/4th
private const double speed_balancing_factor = 40;
- private const double distance_multiplier = 0.94;
+ private const double distance_multiplier = 0.9;
///
/// Evaluates the difficulty of tapping the current object, based on:
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index 5dae9a9fc5..f2e2c2ec5f 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
public class Speed : OsuStrainSkill
{
- private double skillMultiplier => 1.430;
+ private double skillMultiplier => 1.45;
private double strainDecayBase => 0.3;
private double currentStrain;
From 4095b2662bc67da4e3eeb90da0d747b2cc135dcb Mon Sep 17 00:00:00 2001
From: Jay Lawton
Date: Tue, 7 Jan 2025 21:36:56 +1000
Subject: [PATCH 013/262] Add `consistentRatioPenalty` to the `Colour` skill.
(#31285)
* fix colour
* review fix
Co-authored-by: StanR
* remove cancelled out operand
* increase nerf, adjust tests
* fix automated spacing issues
* up penalty
* adjust tests
* apply review changes
* fix nullable hell
---------
Co-authored-by: StanR
---
.../TaikoDifficultyCalculatorTest.cs | 8 +--
.../Difficulty/Evaluators/ColourEvaluator.cs | 54 ++++++++++++++++++-
2 files changed, 57 insertions(+), 5 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index ba247c68d4..de3bec5fcf 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(3.0950934814938953d, 200, "diffcalc-test")]
- [TestCase(3.0950934814938953d, 200, "diffcalc-test-strong")]
+ [TestCase(2.837609165845338d, 200, "diffcalc-test")]
+ [TestCase(2.837609165845338d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(4.0839365008715403d, 200, "diffcalc-test")]
- [TestCase(4.0839365008715403d, 200, "diffcalc-test-strong")]
+ [TestCase(3.8005218640444949, 200, "diffcalc-test")]
+ [TestCase(3.8005218640444949, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
index 25428c8b2f..3ff5b87fb6 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
@@ -36,18 +36,70 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
}
+ ///
+ /// Calculates a consistency penalty based on the number of consecutive consistent intervals,
+ /// considering the delta time between each colour sequence.
+ ///
+ /// The current hitObject to consider.
+ /// The allowable margin of error for determining whether ratios are consistent.
+ /// The maximum objects to check per count of consistent ratio.
+ private static double consistentRatioPenalty(TaikoDifficultyHitObject hitObject, double threshold = 0.01, int maxObjectsToCheck = 64)
+ {
+ int consistentRatioCount = 0;
+ double totalRatioCount = 0.0;
+
+ TaikoDifficultyHitObject current = hitObject;
+
+ for (int i = 0; i < maxObjectsToCheck; i++)
+ {
+ // Break if there is no valid previous object
+ if (current.Index <= 1)
+ break;
+
+ var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1);
+
+ double currentRatio = current.Rhythm.Ratio;
+ double previousRatio = previousHitObject.Rhythm.Ratio;
+
+ // A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error.
+ if (Math.Abs(1 - currentRatio / previousRatio) <= threshold)
+ {
+ consistentRatioCount++;
+ totalRatioCount += currentRatio;
+ break;
+ }
+
+ // Move to the previous object
+ current = previousHitObject;
+ }
+
+ // Ensure no division by zero
+ double ratioPenalty = 1 - totalRatioCount / (consistentRatioCount + 1) * 0.80;
+
+ return ratioPenalty;
+ }
+
+ ///
+ /// Evaluate the difficulty of the first hitobject within a colour streak.
+ ///
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
{
- TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour;
+ var taikoObject = (TaikoDifficultyHitObject)hitObject;
+ TaikoDifficultyHitObjectColour colour = taikoObject.Colour;
double difficulty = 0.0d;
if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
difficulty += EvaluateDifficultyOf(colour.MonoStreak);
+
if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
+
if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);
+ double consistencyPenalty = consistentRatioPenalty(taikoObject);
+ difficulty *= consistencyPenalty;
+
return difficulty;
}
}
From 3b58d5e43565e9b16b94667972ba968dbea36ba1 Mon Sep 17 00:00:00 2001
From: StanR
Date: Tue, 7 Jan 2025 17:49:55 +0500
Subject: [PATCH 014/262] Clamp OD in performance calculation to fix negative
OD gaining pp (#31447)
Co-authored-by: James Wilson
---
.../Difficulty/OsuPerformanceCalculator.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index df418fb3f8..5cf7a56d8a 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= accuracy;
// It is important to consider accuracy difficulty when scaling with accuracy.
- aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
+ aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500;
return aimValue;
}
@@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
// Scale the speed value with accuracy and OD.
- speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
+ speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
// Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
@@ -305,7 +305,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Scale the flashlight value with accuracy _slightly_.
flashlightValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that.
- flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
+ flashlightValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500;
return flashlightValue;
}
From 392bb5718cbbab3a2b3738d460ea3cbbc4d46885 Mon Sep 17 00:00:00 2001
From: StanR
Date: Wed, 8 Jan 2025 15:03:22 +0500
Subject: [PATCH 015/262] Simplify angle bonus formula (#31449)
* Simplify angle bonus formula
* Simplify further
* Simplify acute too
* Tests
---
.../OsuDifficultyCalculatorTest.cs | 6 +++---
.../Difficulty/Evaluators/AimEvaluator.cs | 4 ++--
.../Difficulty/Utils/DifficultyCalculationUtils.cs | 13 +++++++++++++
3 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index 842a34aaa8..fbd865df47 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,20 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
- [TestCase(6.7153612142198682d, 239, "diffcalc-test")]
+ [TestCase(6.7230435389286045d, 239, "diffcalc-test")]
[TestCase(1.4484916289194889d, 54, "zero-length-sliders")]
[TestCase(0.42912495021837549d, 4, "very-fast-slider")]
[TestCase(0.14143808967817237d, 2, "nan-slider")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(9.6358837846598835d, 239, "diffcalc-test")]
+ [TestCase(9.6468019709446171d, 239, "diffcalc-test")]
[TestCase(1.754888327422514d, 54, "zero-length-sliders")]
[TestCase(0.55601568006454294d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
- [TestCase(6.7153612142198682d, 239, "diffcalc-test")]
+ [TestCase(6.7230435389286045d, 239, "diffcalc-test")]
[TestCase(1.4484916289194889d, 54, "zero-length-sliders")]
[TestCase(0.42912495021837549d, 4, "very-fast-slider")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index cff2eae357..8c41240a24 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -142,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
return aimStrain;
}
- private static double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2);
+ private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(30), double.DegreesToRadians(150));
- private static double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle);
+ private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(150), double.DegreesToRadians(30));
}
}
diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
index 497a1f8234..aeccf2fd55 100644
--- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
+++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
@@ -66,6 +66,19 @@ namespace osu.Game.Rulesets.Difficulty.Utils
/// The output of the bell curve function of
public static double BellCurve(double x, double mean, double width, double multiplier = 1.0) => multiplier * Math.Exp(Math.E * -(Math.Pow(x - mean, 2) / Math.Pow(width, 2)));
+ ///
+ /// Smoothstep function (https://en.wikipedia.org/wiki/Smoothstep)
+ ///
+ /// Value to calculate the function for
+ /// Value at which function returns 0
+ /// Value at which function returns 1
+ public static double Smoothstep(double x, double start, double end)
+ {
+ x = Math.Clamp((x - start) / (end - start), 0.0, 1.0);
+
+ return x * x * (3.0 - 2.0 * x);
+ }
+
///
/// Smootherstep function (https://en.wikipedia.org/wiki/Smoothstep#Variations)
///
From db58ec864569889a17952149ff85a05d28a07133 Mon Sep 17 00:00:00 2001
From: StanR
Date: Thu, 9 Jan 2025 14:57:48 +0500
Subject: [PATCH 016/262] Apply a bunch of balancing changes to aim (#31456)
* Apply a bunch of balancing changes to aim
* Update tests
---------
Co-authored-by: James Wilson
---
.../OsuDifficultyCalculatorTest.cs | 18 +++++++++---------
.../Difficulty/Evaluators/AimEvaluator.cs | 8 ++++----
.../Difficulty/Skills/Speed.cs | 2 +-
3 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index fbd865df47..9af5051f45 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
- [TestCase(6.7230435389286045d, 239, "diffcalc-test")]
- [TestCase(1.4484916289194889d, 54, "zero-length-sliders")]
- [TestCase(0.42912495021837549d, 4, "very-fast-slider")]
+ [TestCase(6.6860329680488437d, 239, "diffcalc-test")]
+ [TestCase(1.4485740324170036d, 54, "zero-length-sliders")]
+ [TestCase(0.43052813047866129d, 4, "very-fast-slider")]
[TestCase(0.14143808967817237d, 2, "nan-slider")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(9.6468019709446171d, 239, "diffcalc-test")]
- [TestCase(1.754888327422514d, 54, "zero-length-sliders")]
- [TestCase(0.55601568006454294d, 4, "very-fast-slider")]
+ [TestCase(9.6300773538770041d, 239, "diffcalc-test")]
+ [TestCase(1.7550155729445993d, 54, "zero-length-sliders")]
+ [TestCase(0.55785578988249407d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
- [TestCase(6.7230435389286045d, 239, "diffcalc-test")]
- [TestCase(1.4484916289194889d, 54, "zero-length-sliders")]
- [TestCase(0.42912495021837549d, 4, "very-fast-slider")]
+ [TestCase(6.6860329680488437d, 239, "diffcalc-test")]
+ [TestCase(1.4485740324170036d, 54, "zero-length-sliders")]
+ [TestCase(0.43052813047866129d, 4, "very-fast-slider")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index 8c41240a24..e279ed889a 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
public static class AimEvaluator
{
private const double wide_angle_multiplier = 1.5;
- private const double acute_angle_multiplier = 2.7;
+ private const double acute_angle_multiplier = 2.6;
private const double slider_multiplier = 1.35;
private const double velocity_change_multiplier = 0.75;
private const double wiggle_multiplier = 1.02;
@@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// Penalize angle repetition.
wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
- acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
+ acuteAngleBonus *= 0.1 + 0.9 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
// Apply full wide angle bonus for distance more than one diameter
wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
@@ -142,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
return aimStrain;
}
- private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(30), double.DegreesToRadians(150));
+ private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140));
- private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(150), double.DegreesToRadians(30));
+ private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40));
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index f2e2c2ec5f..bdeea0e918 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
public class Speed : OsuStrainSkill
{
- private double skillMultiplier => 1.45;
+ private double skillMultiplier => 1.46;
private double strainDecayBase => 0.3;
private double currentStrain;
From b21c6457b1a1febd004d508e3597815b64a2a6d4 Mon Sep 17 00:00:00 2001
From: Givikap120 <89256026+Givikap120@users.noreply.github.com>
Date: Thu, 9 Jan 2025 15:27:54 +0200
Subject: [PATCH 017/262] Punish speed PP for scores with high deviation
(#30907)
---
.../Difficulty/OsuDifficultyAttributes.cs | 31 ++++-
.../Difficulty/OsuDifficultyCalculator.cs | 5 +
.../Difficulty/OsuPerformanceAttributes.cs | 3 +
.../Difficulty/OsuPerformanceCalculator.cs | 119 +++++++++++++++++-
.../Difficulty/DifficultyAttributes.cs | 1 +
5 files changed, 149 insertions(+), 10 deletions(-)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
index 3b9a23df23..395f581b65 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
@@ -62,21 +62,33 @@ namespace osu.Game.Rulesets.Osu.Difficulty
///
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
///
- ///
- /// 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; }
///
/// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc).
///
- ///
- /// Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing.
- ///
[JsonProperty("overall_difficulty")]
public double OverallDifficulty { get; set; }
+ ///
+ /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
+ ///
+ [JsonProperty("great_hit_window")]
+ public double GreatHitWindow { get; set; }
+
+ ///
+ /// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc).
+ ///
+ [JsonProperty("ok_hit_window")]
+ public double OkHitWindow { get; set; }
+
+ ///
+ /// The perceived hit window for a MEH hit inclusive of rate-adjusting mods (DT/HT/etc).
+ ///
+ [JsonProperty("meh_hit_window")]
+ public double MehHitWindow { get; set; }
+
///
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
///
@@ -107,6 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
+ yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
if (ShouldSerializeFlashlightDifficulty())
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
@@ -117,6 +130,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
+
+ yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
+ yield return (ATTRIB_ID_MEH_HIT_WINDOW, MehHitWindow);
}
public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo)
@@ -128,12 +144,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
StarRating = values[ATTRIB_ID_DIFFICULTY];
+ GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
+ OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
+ MehHitWindow = values[ATTRIB_ID_MEH_HIT_WINDOW];
DrainRate = onlineInfo.DrainRate;
HitCircleCount = onlineInfo.CircleCount;
SliderCount = onlineInfo.SliderCount;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index d0f23735c3..5a61ea586a 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -99,6 +99,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
+ double hitWindowOk = hitWindows.WindowFor(HitResult.Ok) / clockRate;
+ double hitWindowMeh = hitWindows.WindowFor(HitResult.Meh) / clockRate;
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
{
@@ -114,6 +116,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
SpeedDifficultStrainCount = speedDifficultyStrainCount,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6,
+ GreatHitWindow = hitWindowGreat,
+ OkHitWindow = hitWindowOk,
+ MehHitWindow = hitWindowMeh,
DrainRate = drainRate,
MaxCombo = beatmap.GetMaxCombo(),
HitCircleCount = hitCirclesCount,
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
index 0aeaf7669f..de4491a31b 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
@@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("effective_miss_count")]
public double EffectiveMissCount { get; set; }
+ [JsonProperty("speed_deviation")]
+ public double? SpeedDeviation { get; set; }
+
public override IEnumerable GetAttributesForDisplay()
{
foreach (var attribute in base.GetAttributesForDisplay())
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 5cf7a56d8a..91cd270966 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -9,6 +9,7 @@ using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
+using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Difficulty
{
@@ -40,6 +41,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
///
private double effectiveMissCount;
+ private double? speedDeviation;
+
public OsuPerformanceCalculator()
: base(new OsuRuleset())
{
@@ -110,10 +113,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
}
+ speedDeviation = calculateSpeedDeviation(osuAttributes);
+
double aimValue = computeAimValue(score, osuAttributes);
double speedValue = computeSpeedValue(score, osuAttributes);
double accuracyValue = computeAccuracyValue(score, osuAttributes);
double flashlightValue = computeFlashlightValue(score, osuAttributes);
+
double totalValue =
Math.Pow(
Math.Pow(aimValue, 1.1) +
@@ -129,6 +135,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Accuracy = accuracyValue,
Flashlight = flashlightValue,
EffectiveMissCount = effectiveMissCount,
+ SpeedDeviation = speedDeviation,
Total = totalValue
};
}
@@ -198,7 +205,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
- if (score.Mods.Any(h => h is OsuModRelax))
+ if (score.Mods.Any(h => h is OsuModRelax) || speedDeviation == null)
return 0.0;
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
@@ -230,6 +237,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
}
+ double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
+ speedValue *= speedHighDeviationMultiplier;
+
// Calculate accuracy assuming the worst case scenario
double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
@@ -240,9 +250,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Scale the speed value with accuracy and OD.
speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
- // Scale the speed value with # of 50s to punish doubletapping.
- speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
-
return speedValue;
}
@@ -310,12 +317,116 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue;
}
+ ///
+ /// Estimates player's deviation on speed notes using , assuming worst-case.
+ /// Treats all speed notes as hit circles.
+ ///
+ private double? calculateSpeedDeviation(OsuDifficultyAttributes attributes)
+ {
+ if (totalSuccessfulHits == 0)
+ return null;
+
+ // Calculate accuracy assuming the worst case scenario
+ double speedNoteCount = attributes.SpeedNoteCount;
+ speedNoteCount += (totalHits - attributes.SpeedNoteCount) * 0.1;
+
+ // Assume worst case: all mistakes were on speed notes
+ double relevantCountMiss = Math.Min(countMiss, speedNoteCount);
+ double relevantCountMeh = Math.Min(countMeh, speedNoteCount - relevantCountMiss);
+ double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh);
+ double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk);
+
+ return calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss);
+ }
+
+ ///
+ /// Estimates the player's tap deviation based on the OD, given number of greats, oks, mehs and misses,
+ /// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings
+ /// will always return the same deviation. Misses are ignored because they are usually due to misaiming.
+ /// Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution.
+ ///
+ private double? calculateDeviation(OsuDifficultyAttributes attributes, double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss)
+ {
+ if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0)
+ return null;
+
+ double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss;
+
+ double hitWindowGreat = attributes.GreatHitWindow;
+ double hitWindowOk = attributes.OkHitWindow;
+ double hitWindowMeh = attributes.MehHitWindow;
+
+ // The probability that a player hits a circle is unknown, but we can estimate it to be
+ // the number of greats on circles divided by the number of circles, and then add one
+ // to the number of circles as a bias correction.
+ double n = Math.Max(1, objectCount - relevantCountMiss - relevantCountMeh);
+ const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
+
+ // Proportion of greats hit on circles, ignoring misses and 50s.
+ double p = relevantCountGreat / n;
+
+ // We can be 99% confident that p is at least this value.
+ double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
+
+ // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed.
+ // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than:
+ double deviation = hitWindowGreat / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
+
+ double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2))
+ / (deviation * SpecialFunctions.Erf(hitWindowOk / (Math.Sqrt(2) * deviation)));
+
+ deviation *= Math.Sqrt(1 - randomValue);
+
+ // Value deviation approach as greatCount approaches 0
+ double limitValue = hitWindowOk / Math.Sqrt(3);
+
+ // If precision is not enough to compute true deviation - use limit value
+ if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue)
+ deviation = limitValue;
+
+ // Then compute the variance for mehs.
+ double mehVariance = (hitWindowMeh * hitWindowMeh + hitWindowOk * hitWindowMeh + hitWindowOk * hitWindowOk) / 3;
+
+ // Find the total deviation.
+ deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh));
+
+ return deviation;
+ }
+
+ // Calculates multiplier for speed to account for improper tapping based on the deviation and speed difficulty
+ // https://www.desmos.com/calculator/dmogdhzofn
+ private double calculateSpeedHighDeviationNerf(OsuDifficultyAttributes attributes)
+ {
+ if (speedDeviation == null)
+ return 0;
+
+ double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
+
+ // Decides a point where the PP value achieved compared to the speed deviation is assumed to be tapped improperly. Any PP above this point is considered "excess" speed difficulty.
+ // This is used to cause PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value.
+ double excessSpeedDifficultyCutoff = 100 + 220 * Math.Pow(22 / speedDeviation.Value, 6.5);
+
+ if (speedValue <= excessSpeedDifficultyCutoff)
+ return 1.0;
+
+ const double scale = 50;
+ double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale);
+
+ // 200 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible
+ double lerp = 1 - Math.Clamp((speedDeviation.Value - 20) / (24 - 20), 0, 1);
+ adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp);
+
+ return adjustedSpeedValue / speedValue;
+ }
+
// Miss penalty assumes that a player will miss on the hardest parts of a map,
// so we use the amount of relatively difficult sections to adjust miss penalty
// to make it more punishing on maps with lower amount of hard sections.
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
+
private int totalHits => countGreat + countOk + countMeh + countMiss;
+ private int totalSuccessfulHits => countGreat + countOk + countMeh;
private int totalImperfectHits => countOk + countMeh + countMiss;
}
}
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
index f5ed5a180b..1d6cee043b 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
@@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Difficulty
protected const int ATTRIB_ID_OK_HIT_WINDOW = 27;
protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29;
protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31;
+ protected const int ATTRIB_ID_MEH_HIT_WINDOW = 33;
///
/// The mods which were applied to the beatmap.
From c53188cf450bf5eb9efb903e5e295b7435971386 Mon Sep 17 00:00:00 2001
From: StanR
Date: Tue, 14 Jan 2025 18:18:02 +0500
Subject: [PATCH 018/262] Use total deviation to scale accuracy on aim, general
aim buff (#31498)
* Make aim accuracy scaling harsher
* Use deviation-based scaling
* Bring the balancing multiplier down
* Adjust multipliers, fix incorrect deviation when using slider accuracy
* Adjust multipliers
* Update osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
Co-authored-by: James Wilson
* Change high speed deviation threshold to 22-27 instead of 20-24
* Update tests
---------
Co-authored-by: James Wilson
---
.../OsuDifficultyCalculatorTest.cs | 12 ++--
.../Difficulty/Evaluators/AimEvaluator.cs | 2 +-
.../Difficulty/OsuPerformanceAttributes.cs | 3 +
.../Difficulty/OsuPerformanceCalculator.cs | 57 +++++++++++++++++--
.../Difficulty/Skills/Aim.cs | 2 +-
5 files changed, 63 insertions(+), 13 deletions(-)
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index 9af5051f45..a68d9dad39 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,21 +15,21 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
- [TestCase(6.6860329680488437d, 239, "diffcalc-test")]
- [TestCase(1.4485740324170036d, 54, "zero-length-sliders")]
+ [TestCase(6.7443067697205539d, 239, "diffcalc-test")]
+ [TestCase(1.4630292101418947d, 54, "zero-length-sliders")]
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
[TestCase(0.14143808967817237d, 2, "nan-slider")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(9.6300773538770041d, 239, "diffcalc-test")]
- [TestCase(1.7550155729445993d, 54, "zero-length-sliders")]
+ [TestCase(9.7058844423552308d, 239, "diffcalc-test")]
+ [TestCase(1.7724929629205366d, 54, "zero-length-sliders")]
[TestCase(0.55785578988249407d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
- [TestCase(6.6860329680488437d, 239, "diffcalc-test")]
- [TestCase(1.4485740324170036d, 54, "zero-length-sliders")]
+ [TestCase(6.7443067697205539d, 239, "diffcalc-test")]
+ [TestCase(1.4630292101418947d, 54, "zero-length-sliders")]
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index e279ed889a..858ce673ee 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// Penalize angle repetition.
wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
- acuteAngleBonus *= 0.1 + 0.9 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
+ acuteAngleBonus *= 0.09 + 0.91 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
// Apply full wide angle bonus for distance more than one diameter
wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
index de4491a31b..9c30c0f7c7 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
@@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("effective_miss_count")]
public double EffectiveMissCount { get; set; }
+ [JsonProperty("total_deviation")]
+ public double? TotalDeviation { get; set; }
+
[JsonProperty("speed_deviation")]
public double? SpeedDeviation { get; set; }
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 91cd270966..a03e3fd6ef 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
@@ -41,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
///
private double effectiveMissCount;
+ private double? totalDeviation;
private double? speedDeviation;
public OsuPerformanceCalculator()
@@ -113,6 +115,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
}
+ totalDeviation = calculateTotalDeviation(osuAttributes);
speedDeviation = calculateSpeedDeviation(osuAttributes);
double aimValue = computeAimValue(score, osuAttributes);
@@ -135,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Accuracy = accuracyValue,
Flashlight = flashlightValue,
EffectiveMissCount = effectiveMissCount,
+ TotalDeviation = totalDeviation,
SpeedDeviation = speedDeviation,
Total = totalValue
};
@@ -145,6 +149,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(h => h is OsuModAutopilot))
return 0.0;
+ if (totalDeviation == null)
+ return 0;
+
double aimDifficulty = attributes.AimDifficulty;
if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0)
@@ -196,9 +203,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
}
- aimValue *= accuracy;
- // It is important to consider accuracy difficulty when scaling with accuracy.
- aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500;
+ aimValue *= SpecialFunctions.Erf(25.0 / (Math.Sqrt(2) * totalDeviation.Value));
return aimValue;
}
@@ -317,6 +322,48 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue;
}
+ ///
+ /// Using estimates player's deviation on accuracy objects.
+ /// Returns deviation for circles and sliders if score was set with slideracc.
+ /// Returns the min between deviation of circles and deviation on circles and sliders (assuming slider hits are 50s), if score was set without slideracc.
+ ///
+ private double? calculateTotalDeviation(OsuDifficultyAttributes attributes)
+ {
+ if (totalSuccessfulHits == 0)
+ return null;
+
+ int accuracyObjectCount = attributes.HitCircleCount;
+
+ if (!usingClassicSliderAccuracy)
+ accuracyObjectCount += attributes.SliderCount;
+
+ // Assume worst case: all mistakes was on accuracy objects
+ int relevantCountMiss = Math.Min(countMiss, accuracyObjectCount);
+ int relevantCountMeh = Math.Min(countMeh, accuracyObjectCount - relevantCountMiss);
+ int relevantCountOk = Math.Min(countOk, accuracyObjectCount - relevantCountMiss - relevantCountMeh);
+ int relevantCountGreat = Math.Max(0, accuracyObjectCount - relevantCountMiss - relevantCountMeh - relevantCountOk);
+
+ // Calculate deviation on accuracy objects
+ double? deviation = calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss);
+ if (deviation == null)
+ return null;
+
+ if (!usingClassicSliderAccuracy)
+ return deviation.Value;
+
+ // If score was set without slider accuracy - also compute deviation with sliders
+ // Assume that all hits was 50s
+ int totalCountWithSliders = attributes.HitCircleCount + attributes.SliderCount;
+ int missCountWithSliders = Math.Min(totalCountWithSliders, countMiss);
+ int hitCountWithSliders = totalCountWithSliders - missCountWithSliders;
+
+ double hitProbabilityWithSliders = hitCountWithSliders / (totalCountWithSliders + 1.0);
+ double deviationWithSliders = attributes.MehHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(hitProbabilityWithSliders));
+
+ // Min is needed for edgecase maps with 1 circle and 999 sliders, as deviation on sliders can be lower in this case
+ return Math.Min(deviation.Value, deviationWithSliders);
+ }
+
///
/// Estimates player's deviation on speed notes using , assuming worst-case.
/// Treats all speed notes as hit circles.
@@ -412,8 +459,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
const double scale = 50;
double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale);
- // 200 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible
- double lerp = 1 - Math.Clamp((speedDeviation.Value - 20) / (24 - 20), 0, 1);
+ // 220 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible
+ double lerp = 1 - DifficultyCalculationUtils.ReverseLerp(speedDeviation.Value, 22.0, 27.0);
adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp);
return adjustedSpeedValue / speedValue;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index 400bc97fbc..69211b610f 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double currentStrain;
- private double skillMultiplier => 25.18;
+ private double skillMultiplier => 25.7;
private double strainDecayBase => 0.15;
private readonly List sliderStrains = new List();
From 6cf15e3e5a2f5aa0df42886a60367eb2f184fe30 Mon Sep 17 00:00:00 2001
From: James Wilson
Date: Tue, 14 Jan 2025 18:27:25 +0000
Subject: [PATCH 019/262] Remove problematic total deviation scaling, rebalance
aim (#31515)
* Remove problematic total deviation scaling, rebalance aim
* Fix tests
---
.../OsuDifficultyCalculatorTest.cs | 12 ++---
.../Difficulty/Evaluators/AimEvaluator.cs | 2 +-
.../Difficulty/OsuPerformanceAttributes.cs | 3 --
.../Difficulty/OsuPerformanceCalculator.cs | 52 ++-----------------
.../Difficulty/Skills/Aim.cs | 2 +-
5 files changed, 11 insertions(+), 60 deletions(-)
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index a68d9dad39..7cf5b0529f 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,21 +15,21 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
- [TestCase(6.7443067697205539d, 239, "diffcalc-test")]
- [TestCase(1.4630292101418947d, 54, "zero-length-sliders")]
+ [TestCase(6.7331304290522747d, 239, "diffcalc-test")]
+ [TestCase(1.4602604078137214d, 54, "zero-length-sliders")]
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
[TestCase(0.14143808967817237d, 2, "nan-slider")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(9.7058844423552308d, 239, "diffcalc-test")]
- [TestCase(1.7724929629205366d, 54, "zero-length-sliders")]
+ [TestCase(9.6779397290273756d, 239, "diffcalc-test")]
+ [TestCase(1.7691451263718989d, 54, "zero-length-sliders")]
[TestCase(0.55785578988249407d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
- [TestCase(6.7443067697205539d, 239, "diffcalc-test")]
- [TestCase(1.4630292101418947d, 54, "zero-length-sliders")]
+ [TestCase(6.7331304290522747d, 239, "diffcalc-test")]
+ [TestCase(1.4602604078137214d, 54, "zero-length-sliders")]
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index 858ce673ee..9a5533e536 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// Penalize angle repetition.
wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
- acuteAngleBonus *= 0.09 + 0.91 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
+ acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
// Apply full wide angle bonus for distance more than one diameter
wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
index 9c30c0f7c7..de4491a31b 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
@@ -24,9 +24,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("effective_miss_count")]
public double EffectiveMissCount { get; set; }
- [JsonProperty("total_deviation")]
- public double? TotalDeviation { get; set; }
-
[JsonProperty("speed_deviation")]
public double? SpeedDeviation { get; set; }
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index a03e3fd6ef..7013ee55c4 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
///
private double effectiveMissCount;
- private double? totalDeviation;
private double? speedDeviation;
public OsuPerformanceCalculator()
@@ -115,7 +114,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
}
- totalDeviation = calculateTotalDeviation(osuAttributes);
speedDeviation = calculateSpeedDeviation(osuAttributes);
double aimValue = computeAimValue(score, osuAttributes);
@@ -138,7 +136,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Accuracy = accuracyValue,
Flashlight = flashlightValue,
EffectiveMissCount = effectiveMissCount,
- TotalDeviation = totalDeviation,
SpeedDeviation = speedDeviation,
Total = totalValue
};
@@ -149,9 +146,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(h => h is OsuModAutopilot))
return 0.0;
- if (totalDeviation == null)
- return 0;
-
double aimDifficulty = attributes.AimDifficulty;
if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0)
@@ -203,7 +197,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
}
- aimValue *= SpecialFunctions.Erf(25.0 / (Math.Sqrt(2) * totalDeviation.Value));
+ aimValue *= accuracy;
+ // It is important to consider accuracy difficulty when scaling with accuracy.
+ aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500;
return aimValue;
}
@@ -322,48 +318,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue;
}
- ///
- /// Using estimates player's deviation on accuracy objects.
- /// Returns deviation for circles and sliders if score was set with slideracc.
- /// Returns the min between deviation of circles and deviation on circles and sliders (assuming slider hits are 50s), if score was set without slideracc.
- ///
- private double? calculateTotalDeviation(OsuDifficultyAttributes attributes)
- {
- if (totalSuccessfulHits == 0)
- return null;
-
- int accuracyObjectCount = attributes.HitCircleCount;
-
- if (!usingClassicSliderAccuracy)
- accuracyObjectCount += attributes.SliderCount;
-
- // Assume worst case: all mistakes was on accuracy objects
- int relevantCountMiss = Math.Min(countMiss, accuracyObjectCount);
- int relevantCountMeh = Math.Min(countMeh, accuracyObjectCount - relevantCountMiss);
- int relevantCountOk = Math.Min(countOk, accuracyObjectCount - relevantCountMiss - relevantCountMeh);
- int relevantCountGreat = Math.Max(0, accuracyObjectCount - relevantCountMiss - relevantCountMeh - relevantCountOk);
-
- // Calculate deviation on accuracy objects
- double? deviation = calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss);
- if (deviation == null)
- return null;
-
- if (!usingClassicSliderAccuracy)
- return deviation.Value;
-
- // If score was set without slider accuracy - also compute deviation with sliders
- // Assume that all hits was 50s
- int totalCountWithSliders = attributes.HitCircleCount + attributes.SliderCount;
- int missCountWithSliders = Math.Min(totalCountWithSliders, countMiss);
- int hitCountWithSliders = totalCountWithSliders - missCountWithSliders;
-
- double hitProbabilityWithSliders = hitCountWithSliders / (totalCountWithSliders + 1.0);
- double deviationWithSliders = attributes.MehHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(hitProbabilityWithSliders));
-
- // Min is needed for edgecase maps with 1 circle and 999 sliders, as deviation on sliders can be lower in this case
- return Math.Min(deviation.Value, deviationWithSliders);
- }
-
///
/// Estimates player's deviation on speed notes using , assuming worst-case.
/// Treats all speed notes as hit circles.
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index 69211b610f..f04b679b73 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double currentStrain;
- private double skillMultiplier => 25.7;
+ private double skillMultiplier => 25.6;
private double strainDecayBase => 0.15;
private readonly List sliderStrains = new List();
From 5bed7c22e351a60a9ae22b2f736da4871646911d Mon Sep 17 00:00:00 2001
From: Natelytle <92956514+Natelytle@users.noreply.github.com>
Date: Tue, 14 Jan 2025 14:12:08 -0500
Subject: [PATCH 020/262] Remove lower cap on deviation without misses (#31499)
---
.../Difficulty/TaikoPerformanceCalculator.cs | 48 ++++---------------
1 file changed, 8 insertions(+), 40 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index 5da18e7963..4933c9dee6 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -123,53 +123,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
///
private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes)
{
- if (totalSuccessfulHits == 0 || attributes.GreatHitWindow <= 0)
+ if (countGreat == 0 || attributes.GreatHitWindow <= 0)
return null;
- double h300 = attributes.GreatHitWindow;
- double h100 = attributes.OkHitWindow;
-
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
- double? deviationGreatWindow = calcDeviationGreatWindow();
- double? deviationGoodWindow = calcDeviationGoodWindow();
+ double n = totalHits;
- return deviationGreatWindow is null ? deviationGoodWindow : Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value);
+ // Proportion of greats hit.
+ double p = countGreat / n;
- // The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window.
- double? calcDeviationGreatWindow()
- {
- if (countGreat == 0) return null;
+ // We can be 99% confident that p is at least this value.
+ double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
- double n = totalHits;
-
- // Proportion of greats hit.
- double p = countGreat / n;
-
- // We can be 99% confident that p is at least this value.
- double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
-
- // We can be 99% confident that the deviation is not higher than:
- return h300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
- }
-
- // The upper bound on deviation, calculated with the ratio of 300s + 100s to objects, and the good hit window.
- // This will return a lower value than the first method when the number of 100s is high, but the miss count is low.
- double? calcDeviationGoodWindow()
- {
- if (totalSuccessfulHits == 0) return null;
-
- double n = totalHits;
-
- // Proportion of greats + goods hit.
- double p = Math.Max(0, totalSuccessfulHits - 0.0005 * countOk) / n;
-
- // We can be 99% confident that p is at least this value.
- double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
-
- // We can be 99% confident that the deviation is not higher than:
- return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
- }
+ // We can be 99% confident that the deviation is not higher than:
+ return attributes.GreatHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
}
private int totalHits => countGreat + countOk + countMeh + countMiss;
From 0a21183e54648953b653e2e56b8150a11a93c69a Mon Sep 17 00:00:00 2001
From: Jay Lawton
Date: Wed, 15 Jan 2025 20:34:21 +1000
Subject: [PATCH 021/262] reading mono nerf (#31510)
---
osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs
index 9de058f289..885131404a 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs
@@ -3,6 +3,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.Evaluators;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
@@ -34,6 +35,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
}
var taikoObject = (TaikoDifficultyHitObject)current;
+ int index = taikoObject.Colour.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0;
+
+ currentStrain *= DifficultyCalculationUtils.Logistic(index, 4, -1 / 25.0, 0.5) + 0.5;
currentStrain *= StrainDecayBase;
currentStrain += ReadingEvaluator.EvaluateDifficultyOf(taikoObject) * SkillMultiplier;
From 974fa76987a445f0d0d18f823e11e0bb4ffec842 Mon Sep 17 00:00:00 2001
From: molneya <62799417+molneya@users.noreply.github.com>
Date: Thu, 16 Jan 2025 17:08:47 +0800
Subject: [PATCH 022/262] fix spinners not increasing cumulative strain time
(#31525)
Co-authored-by: StanR
---
.../Difficulty/Evaluators/FlashlightEvaluator.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
index 5cb5a8f934..9d05f0b074 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
@@ -52,12 +52,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
var currentObj = (OsuDifficultyHitObject)current.Previous(i);
var currentHitObject = (OsuHitObject)(currentObj.BaseObject);
+ cumulativeStrainTime += lastObj.StrainTime;
+
if (!(currentObj.BaseObject is Spinner))
{
double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length;
- cumulativeStrainTime += lastObj.StrainTime;
-
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
if (i == 0)
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
From 9da8dcd8151009a2252c9b3f45d258f92a501895 Mon Sep 17 00:00:00 2001
From: Jay Lawton
Date: Thu, 16 Jan 2025 20:30:02 +1000
Subject: [PATCH 023/262] osu!taiko stamina balancing (#31337)
* stamina considerations
* remove consecutive note count
* adjust multiplier
* add back comment
* adjust tests
* adjusts tests post merge
* use diffcalcutils
---------
Co-authored-by: StanR
---
.../TaikoDifficultyCalculatorTest.cs | 8 ++++----
.../Difficulty/Evaluators/StaminaEvaluator.cs | 17 ++++++++---------
.../Difficulty/Skills/Stamina.cs | 9 ++++++---
3 files changed, 18 insertions(+), 16 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index de3bec5fcf..517f62b6f5 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(2.837609165845338d, 200, "diffcalc-test")]
- [TestCase(2.837609165845338d, 200, "diffcalc-test-strong")]
+ [TestCase(2.912326627861987d, 200, "diffcalc-test")]
+ [TestCase(2.912326627861987d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(3.8005218640444949, 200, "diffcalc-test")]
- [TestCase(3.8005218640444949, 200, "diffcalc-test-strong")]
+ [TestCase(3.9339069955362014d, 200, "diffcalc-test")]
+ [TestCase(3.9339069955362014d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
index 84d5de4c63..a273d91a38 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
// Interval is capped at a very small value to prevent infinite values.
interval = Math.Max(interval, 1);
- return 30 / interval;
+ return 20 / interval;
}
///
@@ -59,16 +59,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
// Find the previous hit object hit by the current finger, which is n notes prior, n being the number of
// available fingers.
TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current;
- TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);
-
- if (keyPrevious == null)
- {
- // There is no previous hit object hit by the current finger
- return 0.0;
- }
+ TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject;
+ TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);
double objectStrain = 0.5; // Add a base strain to all objects
- objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime);
+ if (taikoPrevious == null) return objectStrain;
+
+ if (previousMono != null)
+ objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime);
+
return objectStrain;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
index f6914039f0..29f9f16033 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
@@ -4,6 +4,7 @@
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.Difficulty.Preprocessing;
@@ -44,10 +45,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
var currentObject = current as TaikoDifficultyHitObject;
int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0;
- if (singleColourStamina)
- return currentStrain / (1 + Math.Exp(-(index - 10) / 2.0));
+ double monolengthBonus = 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30);
- return currentStrain;
+ if (singleColourStamina)
+ return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain);
+
+ return currentStrain * monolengthBonus;
}
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => singleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime);
From b9894f67ceac3ba42995cd81b0692c414620053f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Thu, 16 Jan 2025 12:30:27 +0100
Subject: [PATCH 024/262] Bump NVika tool to 4.0.0
Code quality CI runs have suddenly started failing out of nowhere:
- Passing run: https://github.com/ppy/osu/actions/runs/12806242929/job/35704267944#step:10:1
- Failing run: https://github.com/ppy/osu/actions/runs/12807108792/job/35707131634#step:10:1
In classic github fashion, they began rolling out another runner change
wherein `ubuntu-latest` has started meaning `ubuntu-24.04` rather than
`ubuntu-22.04`. `ubuntu-24.04` no longer has .NET 6 bundled.
Therefore, upgrade NVika to 4.0.0 because that version is compatible
with .NET 8.
---
.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 c4ba6e5143..6ec071be2f 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -9,7 +9,7 @@
]
},
"nvika": {
- "version": "3.0.0",
+ "version": "4.0.0",
"commands": [
"nvika"
]
From a83f917d87c51f95d2778afce0048c08f8af125f Mon Sep 17 00:00:00 2001
From: Jay Lawton
Date: Fri, 17 Jan 2025 07:14:05 +1000
Subject: [PATCH 025/262] osu!taiko star rating and performance points
rebalance (#31338)
* rebalance
* revert pp scaling change
* further rebalancing
* comment
* adjust tests
---
.../TaikoDifficultyCalculatorTest.cs | 8 +++---
.../Difficulty/TaikoDifficultyAttributes.cs | 4 +--
.../Difficulty/TaikoDifficultyCalculator.cs | 27 +++++++++++++------
.../Difficulty/TaikoPerformanceCalculator.cs | 8 +++---
4 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index 517f62b6f5..b4cbe03511 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(2.912326627861987d, 200, "diffcalc-test")]
- [TestCase(2.912326627861987d, 200, "diffcalc-test-strong")]
+ [TestCase(3.3172381854905493d, 200, "diffcalc-test")]
+ [TestCase(3.3172381854905493d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(3.9339069955362014d, 200, "diffcalc-test")]
- [TestCase(3.9339069955362014d, 200, "diffcalc-test-strong")]
+ [TestCase(4.4640702427013101d, 200, "diffcalc-test")]
+ [TestCase(4.4640702427013101d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
index ef729e1f07..37e6996e5a 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
@@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
[JsonProperty("mono_stamina_factor")]
public double MonoStaminaFactor { get; set; }
- [JsonProperty("reading_difficult_strains")]
- public double ReadingTopStrains { get; set; }
+ [JsonProperty("rhythm_difficult_strains")]
+ public double RhythmTopStrains { get; set; }
[JsonProperty("colour_difficult_strains")]
public double ColourTopStrains { get; set; }
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index f8ff6f6065..3ad9d17526 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -24,10 +24,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
public class TaikoDifficultyCalculator : DifficultyCalculator
{
private const double difficulty_multiplier = 0.084375;
- private const double rhythm_skill_multiplier = 1.24 * difficulty_multiplier;
+ private const double rhythm_skill_multiplier = 0.65 * difficulty_multiplier;
private const double reading_skill_multiplier = 0.100 * difficulty_multiplier;
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
- private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier;
+ private const double stamina_skill_multiplier = 0.445 * difficulty_multiplier;
+
+ private double strainLengthBonus;
+ private double patternMultiplier;
public override int Version => 20241007;
@@ -116,8 +119,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5);
double colourDifficultStrains = colour.CountTopWeightedStrains();
- double readingDifficultStrains = reading.CountTopWeightedStrains();
- double staminaDifficultStrains = stamina.CountTopWeightedStrains();
+ double rhythmDifficultStrains = rhythm.CountTopWeightedStrains();
+ // Due to constraints of strain in cases where difficult strain values don't shift with range changes, we manually apply clockrate.
+ double staminaDifficultStrains = stamina.CountTopWeightedStrains() * clockRate;
+
+ // As we don't have pattern integration in osu!taiko, we apply the other two skills relative to rhythm.
+ patternMultiplier = Math.Pow(staminaRating * colourRating, 0.10);
+
+ strainLengthBonus = 1
+ + Math.Min(Math.Max((staminaDifficultStrains - 1350) / 5000, 0), 0.15)
+ + Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05);
double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax);
double starRating = rescale(combinedRating * 1.4);
@@ -125,7 +136,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
// Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope.
if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0)
{
- starRating *= 0.825;
+ starRating *= 0.7;
// For maps with relax, multiple inputs are more likely to be abused.
if (isRelax)
@@ -144,7 +155,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
ColourDifficulty = colourRating,
StaminaDifficulty = staminaRating,
MonoStaminaFactor = monoStaminaFactor,
- ReadingTopStrains = readingDifficultStrains,
+ RhythmTopStrains = rhythmDifficultStrains,
ColourTopStrains = colourDifficultStrains,
StaminaTopStrains = staminaDifficultStrains,
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
@@ -173,10 +184,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
for (int i = 0; i < colourPeaks.Count; i++)
{
- double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
+ double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier * patternMultiplier;
double readingPeak = readingPeaks[i] * reading_skill_multiplier;
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
- double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
+ double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * strainLengthBonus;
if (isRelax)
{
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index 4933c9dee6..c29ea3ba73 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -73,8 +73,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
{
- double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0;
- double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1150.0);
+ double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.110) - 4.0;
+ double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1250.0);
+
+ difficultyValue *= 1 + 0.10 * Math.Max(0, attributes.StarRating - 10);
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
difficultyValue *= lengthBonus;
@@ -95,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
// Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps.
double accScalingExponent = 2 + attributes.MonoStaminaFactor;
- double accScalingShift = 400 - 100 * attributes.MonoStaminaFactor;
+ double accScalingShift = 500 - 100 * attributes.MonoStaminaFactor;
return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent);
}
From ad422295c85d257044edd33dba7284b7c8d9b631 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Fri, 10 Jan 2025 21:38:37 +0900
Subject: [PATCH 026/262] Add ctor to create Rooms from MultiplayerRooms
---
osu.Game/Online/Rooms/Room.cs | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs
index f8660a656e..7647134646 100644
--- a/osu.Game/Online/Rooms/Room.cs
+++ b/osu.Game/Online/Rooms/Room.cs
@@ -342,6 +342,29 @@ namespace osu.Game.Online.Rooms
// Not yet serialised (not implemented).
private RoomAvailability availability;
+ public Room()
+ {
+ }
+
+ ///
+ /// Creates a from a .
+ ///
+ public Room(MultiplayerRoom room)
+ {
+ RoomID = room.RoomID;
+ Host = room.Host?.User;
+
+ Name = room.Settings.Name;
+ Password = room.Settings.Password;
+ Type = room.Settings.MatchType;
+ QueueMode = room.Settings.QueueMode;
+ AutoStartDuration = room.Settings.AutoStartDuration;
+ AutoSkip = room.Settings.AutoSkip;
+
+ Playlist = room.Playlist.Select(item => new PlaylistItem(item)).ToArray();
+ CurrentPlaylistItem = Playlist.FirstOrDefault(item => item.ID == room.Settings.PlaylistItemId);
+ }
+
///
/// Copies values from another into this one.
///
From 3d2d4ee89f06a88feabcfdda1b73ac1cbeaf1c49 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Fri, 10 Jan 2025 22:07:13 +0900
Subject: [PATCH 027/262] Add ctor to create MultiplayerPlaylistItem from
PlaylistItem
---
osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs
index 8be703e620..6e467c1d26 100644
--- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs
+++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs
@@ -60,5 +60,20 @@ namespace osu.Game.Online.Rooms
public MultiplayerPlaylistItem()
{
}
+
+ public MultiplayerPlaylistItem(PlaylistItem item)
+ {
+ ID = item.ID;
+ OwnerID = item.OwnerID;
+ BeatmapID = item.Beatmap.OnlineID;
+ BeatmapChecksum = item.Beatmap.MD5Hash;
+ RulesetID = item.RulesetID;
+ RequiredMods = item.RequiredMods.ToArray();
+ AllowedMods = item.AllowedMods.ToArray();
+ Expired = item.Expired;
+ PlaylistOrder = item.PlaylistOrder ?? 0;
+ PlayedAt = item.PlayedAt;
+ StarRating = item.Beatmap.StarRating;
+ }
}
}
From a42c03cea457b9e6786983d77d966a461d1a10ed Mon Sep 17 00:00:00 2001
From: Jay Lawton
Date: Fri, 17 Jan 2025 21:15:22 +1000
Subject: [PATCH 028/262] osu!taiko further considerations for rhythm (#31339)
* further considerations for rhythm
* new rhythm balancing
* fix license header
* use isNormal to validate ratio
* adjust tests
---------
Co-authored-by: StanR
---
.../TaikoDifficultyCalculatorTest.cs | 8 ++--
.../Difficulty/Evaluators/RhythmEvaluator.cs | 48 +++++++++++++------
2 files changed, 38 insertions(+), 18 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index b4cbe03511..d760b9aef6 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(3.3172381854905493d, 200, "diffcalc-test")]
- [TestCase(3.3172381854905493d, 200, "diffcalc-test-strong")]
+ [TestCase(3.3167800835687551d, 200, "diffcalc-test")]
+ [TestCase(3.3167800835687551d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(4.4640702427013101d, 200, "diffcalc-test")]
- [TestCase(4.4640702427013101d, 200, "diffcalc-test-strong")]
+ [TestCase(4.4631326105105122d, 200, "diffcalc-test")]
+ [TestCase(4.4631326105105122d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
index 3a294f7123..7d58eada5e 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
@@ -21,27 +21,39 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power);
}
+ ///
+ /// Validates the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions.
+ ///
+ private static double validateRatio(double ratio)
+ {
+ return double.IsNormal(ratio) ? ratio : 0;
+ }
+
///
/// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses.
///
private static double ratioDifficulty(double ratio, int terms = 8)
{
double difficulty = 0;
+ ratio = validateRatio(ratio);
for (int i = 1; i <= terms; ++i)
{
- difficulty += termPenalty(ratio, i, 2, 1);
+ difficulty += termPenalty(ratio, i, 4, 1);
}
- difficulty += terms;
+ difficulty += terms / (1 + ratio);
// Give bonus to near-1 ratios
- difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.7);
+ difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5);
// Penalize ratios that are VERY near 1
- difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5);
+ difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3);
- return difficulty / Math.Sqrt(8);
+ difficulty = Math.Max(difficulty, 0);
+ difficulty /= Math.Sqrt(8);
+
+ return difficulty;
}
///
@@ -55,10 +67,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
? sameInterval(sameRhythmHitObjects, 4)
: 1.0; // Returns a non-penalty if there are 6 or more notes within an interval.
- // Scale penalties dynamically based on hit object duration relative to hitWindow.
- double penaltyScaling = Math.Max(1 - sameRhythmHitObjects.Duration / (hitWindow * 2), 0.5);
+ // The duration penalty is based on hit object duration relative to hitWindow.
+ double durationPenalty = Math.Max(1 - sameRhythmHitObjects.Duration * 2 / hitWindow, 0.5);
- return Math.Min(longIntervalPenalty, shortIntervalPenalty) * penaltyScaling;
+ return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty;
double sameInterval(SameRhythmHitObjects startObject, int intervalCount)
{
@@ -82,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{
double ratio = intervals[i]!.Value / intervals[j]!.Value;
if (Math.Abs(1 - ratio) <= threshold) // If any two intervals are similar, apply a penalty.
- return 0.3;
+ return 0.80;
}
}
@@ -95,6 +107,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio);
double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval;
+ intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow);
+
// If a previous interval exists and there are multiple hit objects in the sequence:
if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1)
{
@@ -111,9 +125,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
}
}
- // Apply consistency penalty.
- intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow);
-
// Penalise patterns that can be hit within a single hit window.
intervalDifficulty *= DifficultyCalculationUtils.Logistic(
sameRhythmHitObjects.Duration / hitWindow,
@@ -137,11 +148,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm;
double difficulty = 0.0d;
+ double sameRhythm = 0;
+ double samePattern = 0;
+ double intervalPenalty = 0;
+
if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects
- difficulty += evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow);
+ {
+ sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow);
+ intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow);
+ }
if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns
- difficulty += 0.5 * evaluateDifficultyOf(rhythm.SamePatterns);
+ samePattern += 1.15 * evaluateDifficultyOf(rhythm.SamePatterns);
+
+ difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty;
return difficulty;
}
From ad28de8ae3aa1d2817fc8511929d52c2c3ab0b20 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Fri, 17 Jan 2025 21:44:40 +0900
Subject: [PATCH 029/262] Create multiplayer rooms via multiplayer server
---
.../Multiplayer/IMultiplayerLoungeServer.cs | 2 +
.../Online/Multiplayer/MultiplayerClient.cs | 42 ++++++++++++------
.../Online/Multiplayer/MultiplayerRoom.cs | 9 ++++
.../Multiplayer/MultiplayerRoomSettings.cs | 14 ++++++
.../Multiplayer/OnlineMultiplayerClient.cs | 25 +++++++++++
osu.Game/Online/Rooms/Room.cs | 23 ----------
.../Match/MultiplayerMatchSettingsOverlay.cs | 44 +++++++++----------
.../Multiplayer/MultiplayerMatchSubScreen.cs | 5 +--
.../Multiplayer/TestMultiplayerClient.cs | 5 +++
9 files changed, 105 insertions(+), 64 deletions(-)
diff --git a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs
index f266c38b8b..c5eb6f9b36 100644
--- a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs
+++ b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs
@@ -10,6 +10,8 @@ namespace osu.Game.Online.Multiplayer
///
public interface IMultiplayerLoungeServer
{
+ Task CreateRoom(MultiplayerRoom room);
+
///
/// Request to join a multiplayer room.
///
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 4a28124583..d0c3a1fa06 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -165,6 +165,15 @@ namespace osu.Game.Online.Multiplayer
private readonly TaskChain joinOrLeaveTaskChain = new TaskChain();
private CancellationTokenSource? joinCancellationSource;
+ public async Task CreateRoom(Room room)
+ {
+ if (Room != null)
+ throw new InvalidOperationException("Cannot join a multiplayer room while already in one.");
+
+ var cancellationSource = joinCancellationSource = new CancellationTokenSource();
+ await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token);
+ }
+
///
/// Joins the for a given API .
///
@@ -175,34 +184,34 @@ namespace osu.Game.Online.Multiplayer
if (Room != null)
throw new InvalidOperationException("Cannot join a multiplayer room while already in one.");
- var cancellationSource = joinCancellationSource = new CancellationTokenSource();
+ Debug.Assert(room.RoomID != null);
+ var cancellationSource = joinCancellationSource = new CancellationTokenSource();
+ await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token);
+ }
+
+ private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken)
+ {
await joinOrLeaveTaskChain.Add(async () =>
{
- Debug.Assert(room.RoomID != null);
-
- // Join the server-side room.
- var joinedRoom = await JoinRoom(room.RoomID.Value, password ?? room.Password).ConfigureAwait(false);
- Debug.Assert(joinedRoom != null);
+ // Initialise the server-side room.
+ MultiplayerRoom joinedRoom = await initFunc(room).ConfigureAwait(false);
// Populate users.
- Debug.Assert(joinedRoom.Users != null);
await PopulateUsers(joinedRoom.Users).ConfigureAwait(false);
// Update the stored room (must be done on update thread for thread-safety).
await runOnUpdateThreadAsync(() =>
{
Debug.Assert(Room == null);
+ Debug.Assert(APIRoom == null);
Room = joinedRoom;
APIRoom = room;
- Debug.Assert(joinedRoom.Playlist.Count > 0);
-
+ APIRoom.RoomID = joinedRoom.RoomID;
APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray();
APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
-
- // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client.
APIRoom.EndDate = null;
Debug.Assert(LocalUser != null);
@@ -216,8 +225,8 @@ namespace osu.Game.Online.Multiplayer
postServerShuttingDownNotification();
OnRoomJoined();
- }, cancellationSource.Token).ConfigureAwait(false);
- }, cancellationSource.Token).ConfigureAwait(false);
+ }, cancellationToken).ConfigureAwait(false);
+ }, cancellationToken).ConfigureAwait(false);
}
///
@@ -227,6 +236,13 @@ namespace osu.Game.Online.Multiplayer
{
}
+ ///
+ /// Creates the with the given settings.
+ ///
+ /// The room.
+ /// The joined
+ protected abstract Task CreateRoom(MultiplayerRoom room);
+
///
/// Joins the with a given ID.
///
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs
index 00048fa931..f7bd4490ff 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using MessagePack;
using Newtonsoft.Json;
using osu.Game.Online.Rooms;
@@ -65,6 +66,14 @@ namespace osu.Game.Online.Multiplayer
RoomID = roomId;
}
+ public MultiplayerRoom(Room room)
+ {
+ RoomID = room.RoomID ?? 0;
+ Settings = new MultiplayerRoomSettings(room);
+ Host = room.Host != null ? new MultiplayerRoomUser(room.Host.OnlineID) : null;
+ Playlist = room.Playlist.Select(p => new MultiplayerPlaylistItem(p)).ToArray();
+ }
+
public override string ToString() => $"RoomID:{RoomID} Host:{Host?.UserID} Users:{Users.Count} State:{State} Settings: [{Settings}]";
}
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
index c73b02874e..c264ec1eef 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs
@@ -35,6 +35,20 @@ namespace osu.Game.Online.Multiplayer
[IgnoreMember]
public bool AutoStartEnabled => AutoStartDuration != TimeSpan.Zero;
+ public MultiplayerRoomSettings()
+ {
+ }
+
+ public MultiplayerRoomSettings(Room room)
+ {
+ Name = room.Name;
+ Password = room.Password ?? string.Empty;
+ MatchType = room.Type;
+ QueueMode = room.QueueMode;
+ AutoStartDuration = room.AutoStartDuration;
+ AutoSkip = room.AutoSkip;
+ }
+
public bool Equals(MultiplayerRoomSettings? other)
{
if (ReferenceEquals(this, other)) return true;
diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
index 40436d730e..524873ef66 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -266,6 +266,31 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId);
}
+ protected override async Task CreateRoom(MultiplayerRoom room)
+ {
+ if (!IsConnected.Value)
+ throw new OperationCanceledException();
+
+ Debug.Assert(connection != null);
+
+ try
+ {
+ return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room);
+ }
+ catch (HubException exception)
+ {
+ if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
+ {
+ Debug.Assert(connector != null);
+
+ await connector.Reconnect().ConfigureAwait(false);
+ return await CreateRoom(room).ConfigureAwait(false);
+ }
+
+ throw;
+ }
+ }
+
public override Task DisconnectInternal()
{
if (connector == null)
diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs
index 7647134646..f8660a656e 100644
--- a/osu.Game/Online/Rooms/Room.cs
+++ b/osu.Game/Online/Rooms/Room.cs
@@ -342,29 +342,6 @@ namespace osu.Game.Online.Rooms
// Not yet serialised (not implemented).
private RoomAvailability availability;
- public Room()
- {
- }
-
- ///
- /// Creates a from a .
- ///
- public Room(MultiplayerRoom room)
- {
- RoomID = room.RoomID;
- Host = room.Host?.User;
-
- Name = room.Settings.Name;
- Password = room.Settings.Password;
- Type = room.Settings.MatchType;
- QueueMode = room.Settings.QueueMode;
- AutoStartDuration = room.Settings.AutoStartDuration;
- AutoSkip = room.Settings.AutoSkip;
-
- Playlist = room.Playlist.Select(item => new PlaylistItem(item)).ToArray();
- CurrentPlaylistItem = Playlist.FirstOrDefault(item => item.ID == room.Settings.PlaylistItemId);
- }
-
///
/// Copies values from another into this one.
///
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
index 1372054149..279b140d36 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
@@ -29,12 +29,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public partial class MultiplayerMatchSettingsOverlay : RoomSettingsOverlay
{
- public required Bindable SelectedItem
- {
- get => selectedItem;
- set => selectedItem.Current = value;
- }
-
protected override OsuButton SubmitButton => settings.ApplyButton;
protected override bool IsLoading => ongoingOperationTracker.InProgress.Value;
@@ -56,7 +50,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y,
SettingsApplied = Hide,
- SelectedItem = { BindTarget = SelectedItem }
};
protected partial class MatchSettings : CompositeDrawable
@@ -65,7 +58,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
- public readonly Bindable SelectedItem = new Bindable();
public Action? SettingsApplied;
public OsuTextBox NameField = null!;
@@ -86,9 +78,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
[Resolved]
private MultiplayerMatchSubScreen matchSubScreen { get; set; } = null!;
- [Resolved]
- private IRoomManager manager { get; set; } = null!;
-
[Resolved]
private MultiplayerClient client { get; set; } = null!;
@@ -279,7 +268,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
RelativeSizeAxes = Axes.X,
Height = DrawableRoomPlaylistItem.HEIGHT,
- SelectedItem = { BindTarget = SelectedItem }
},
selectBeatmapButton = new RoundedButton
{
@@ -482,19 +470,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}
else
{
- room.Name = NameField.Text;
- room.Type = TypePicker.Current.Value;
- room.Password = PasswordTextBox.Current.Value;
- room.QueueMode = QueueModeDropdown.Current.Value;
- room.AutoStartDuration = TimeSpan.FromSeconds((int)startModeDropdown.Current.Value);
- room.AutoSkip = AutoSkipCheckbox.Current.Value;
+ client.CreateRoom(room).ContinueWith(t => Schedule(() =>
+ {
+ if (t.IsCompleted)
+ onSuccess(room);
+ else if (t.IsFaulted)
+ {
+ Exception? exception = t.Exception;
- if (int.TryParse(MaxParticipantsField.Text, out int max))
- room.MaxParticipants = max;
- else
- room.MaxParticipants = null;
+ if (exception is AggregateException ae)
+ exception = ae.InnerException;
- manager.CreateRoom(room, onSuccess, onError);
+ Debug.Assert(exception != null);
+
+ if (exception.GetHubExceptionMessage() is string message)
+ onError(message);
+ else
+ onError($"Error creating room: {exception}");
+ }
+ else
+ onError("Error creating room.");
+ }));
}
}
@@ -520,7 +516,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
if (text.StartsWith(not_found_prefix, StringComparison.Ordinal))
{
ErrorText.Text = "The selected beatmap is not available online.";
- SelectedItem.Value?.MarkInvalid();
+ room.Playlist.SingleOrDefault()?.MarkInvalid();
}
else
{
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index edc45dbf7c..06ea5ee033 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -233,10 +233,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
SelectedItem = SelectedItem
};
- protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room)
- {
- SelectedItem = SelectedItem
- };
+ protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room);
protected override void UpdateMods()
{
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 4d812abf11..70e298f3e0 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -483,6 +483,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId));
+ protected override Task CreateRoom(MultiplayerRoom room)
+ {
+ throw new NotImplementedException();
+ }
+
private async Task changeMatchType(MatchType type)
{
Debug.Assert(ServerRoom != null);
From 5b4ba9225d7810c21a2456c9824e2a3fe621306a Mon Sep 17 00:00:00 2001
From: Natelytle <92956514+Natelytle@users.noreply.github.com>
Date: Fri, 17 Jan 2025 14:37:34 -0500
Subject: [PATCH 030/262] Move error function from osu.Game.Utils to
osu.Game.Rulesets.Difficulty.Utils (#31520)
* Move error function implementation to osu.Game.Rulesets.Difficulty.Utils
* Rename ErrorFunction.cs to DifficultyCalculationUtils_ErrorFunction.cs
---
.../Difficulty/OsuPerformanceCalculator.cs | 5 ++---
.../Difficulty/TaikoPerformanceCalculator.cs | 6 +++---
.../Difficulty/Utils/DifficultyCalculationUtils.cs | 2 +-
.../Utils/DifficultyCalculationUtils_ErrorFunction.cs} | 7 ++-----
4 files changed, 8 insertions(+), 12 deletions(-)
rename osu.Game/{Utils/SpecialFunctions.cs => Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs} (99%)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 7013ee55c4..f191180630 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -10,7 +10,6 @@ using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
-using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Difficulty
{
@@ -371,10 +370,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed.
// Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than:
- double deviation = hitWindowGreat / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
+ double deviation = hitWindowGreat / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound));
double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2))
- / (deviation * SpecialFunctions.Erf(hitWindowOk / (Math.Sqrt(2) * deviation)));
+ / (deviation * DifficultyCalculationUtils.Erf(hitWindowOk / (Math.Sqrt(2) * deviation)));
deviation *= Math.Sqrt(1 - randomValue);
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index c29ea3ba73..9e7bf7cb7a 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -5,11 +5,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Scoring;
-using osu.Game.Utils;
namespace osu.Game.Rulesets.Taiko.Difficulty
{
@@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double accScalingExponent = 2 + attributes.MonoStaminaFactor;
double accScalingShift = 500 - 100 * attributes.MonoStaminaFactor;
- return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent);
+ return difficultyValue * Math.Pow(DifficultyCalculationUtils.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent);
}
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
@@ -139,7 +139,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
// We can be 99% confident that the deviation is not higher than:
- return attributes.GreatHitWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
+ return attributes.GreatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound));
}
private int totalHits => countGreat + countOk + countMeh + countMiss;
diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
index aeccf2fd55..78df8a139b 100644
--- a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
+++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs
@@ -6,7 +6,7 @@ using System.Linq;
namespace osu.Game.Rulesets.Difficulty.Utils
{
- public static class DifficultyCalculationUtils
+ public static partial class DifficultyCalculationUtils
{
///
/// Converts BPM value into milliseconds
diff --git a/osu.Game/Utils/SpecialFunctions.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs
similarity index 99%
rename from osu.Game/Utils/SpecialFunctions.cs
rename to osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs
index 795a84a973..4b89cbe7cc 100644
--- a/osu.Game/Utils/SpecialFunctions.cs
+++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils_ErrorFunction.cs
@@ -3,7 +3,6 @@
// All code is referenced from the following:
// https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/SpecialFunctions/Erf.cs
-// https://github.com/mathnet/mathnet-numerics/blob/master/src/Numerics/Optimization/NelderMeadSimplex.cs
/*
Copyright (c) 2002-2022 Math.NET
@@ -14,12 +13,10 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
using System;
-namespace osu.Game.Utils
+namespace osu.Game.Rulesets.Difficulty.Utils
{
- public class SpecialFunctions
+ public partial class DifficultyCalculationUtils
{
- private const double sqrt2_pi = 2.5066282746310005024157652848110452530069867406099d;
-
///
/// **************************************
/// COEFFICIENTS FOR METHOD ErfImp *
From 8354cd5f93a7c1989dbee48fb0c1403b96a7b420 Mon Sep 17 00:00:00 2001
From: Eloise
Date: Sat, 18 Jan 2025 13:52:47 +0000
Subject: [PATCH 031/262] Penalise the reading difficulty of high velocity
notes using "note density" (#31512)
* Penalise reading difficulty of high velocity notes at high densities
* Use System for math functions
* Lawtrohux changes
* Clean up density penalty comment
* Swap midVelocity and highVelocity back around
* code quality pass
---------
Co-authored-by: Jay Lawton
Co-authored-by: StanR
---
.../Difficulty/Evaluators/ReadingEvaluator.cs | 21 +++++++++++++++----
1 file changed, 17 insertions(+), 4 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs
index a6a1513842..2a08f65c7b 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.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.Utils;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
@@ -31,13 +32,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
/// The reading difficulty value for the given hit object.
public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject)
{
- double effectiveBPM = noteObject.EffectiveBPM;
-
var highVelocity = new VelocityRange(480, 640);
var midVelocity = new VelocityRange(360, 480);
- return 1.0 * DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center, 1.0 / (highVelocity.Range / 10))
- + 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10));
+ // Apply a cap to prevent outlier values on maps that exceed the editor's parameters.
+ double effectiveBPM = Math.Max(1.0, noteObject.EffectiveBPM);
+
+ double midVelocityDifficulty = 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10));
+
+ // Expected DeltaTime is the DeltaTime this note would need to be spaced equally to a base slider velocity 1/4 note.
+ double expectedDeltaTime = 21000.0 / effectiveBPM;
+ double objectDensity = expectedDeltaTime / Math.Max(1.0, noteObject.DeltaTime);
+
+ // High density is penalised at high velocity as it is generally considered easier to read. See https://www.desmos.com/calculator/u63f3ntdsi
+ double densityPenalty = DifficultyCalculationUtils.Logistic(objectDensity, 0.925, 15);
+
+ double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty) * DifficultyCalculationUtils.Logistic
+ (effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10));
+
+ return midVelocityDifficulty + highVelocityDifficulty;
}
}
}
From 67723b3e5201f8b10e2aaac8831c4f4960e934ba Mon Sep 17 00:00:00 2001
From: "Bastien D." <37190278+bastoo0@users.noreply.github.com>
Date: Sat, 18 Jan 2025 20:26:23 +0100
Subject: [PATCH 032/262] Fix osu!catch "buzz slider" SR abuse (#31126)
* Implement fix for catch buzz sliders SR abuse
* Run formatting
---------
Co-authored-by: StanR
---
.../Difficulty/Skills/Movement.cs | 25 ++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index 54b85f1745..2d1adbd056 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -26,7 +26,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
private float? lastPlayerPosition;
private float lastDistanceMoved;
+ private float lastExactDistanceMoved;
private double lastStrainTime;
+ private bool isBuzzSliderTriggered;
///
/// The speed multiplier applied to the player's catcher.
@@ -59,6 +61,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
float distanceMoved = playerPosition - lastPlayerPosition.Value;
+ // For the exact position we consider that the catcher is in the correct position for both objects
+ float exactDistanceMoved = catchCurrent.NormalizedPosition - lastPlayerPosition.Value;
+
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
@@ -92,12 +97,30 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
playerPosition = catchCurrent.NormalizedPosition;
}
- distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
+ distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20)
+ * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
+ }
+
+ // There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
+ // the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and normalized_hitobject_radius offsets
+ // We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
+ // To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius)
+ if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime)
+ {
+ if (isBuzzSliderTriggered)
+ distanceAddition = 0;
+ else
+ isBuzzSliderTriggered = true;
+ }
+ else
+ {
+ isBuzzSliderTriggered = false;
}
lastPlayerPosition = playerPosition;
lastDistanceMoved = distanceMoved;
lastStrainTime = catchCurrent.StrainTime;
+ lastExactDistanceMoved = exactDistanceMoved;
return distanceAddition / weightedStrainTime;
}
From e320f17fafa8d904bb7a436971feabaeb3f64e3b Mon Sep 17 00:00:00 2001
From: James Wilson
Date: Sun, 19 Jan 2025 15:47:39 +0000
Subject: [PATCH 033/262] Remove redundant angle check (#31566)
---
osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs | 2 +-
osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index 7cf5b0529f..defd02b830 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(9.6779397290273756d, 239, "diffcalc-test")]
+ [TestCase(9.6779746353001634d, 239, "diffcalc-test")]
[TestCase(1.7691451263718989d, 54, "zero-length-sliders")]
[TestCase(0.55785578988249407d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index 9a5533e536..d1c92ed6a7 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same.
{
- if (osuCurrObj.Angle != null && osuLastObj.Angle != null && osuLastLastObj.Angle != null)
+ if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
{
double currAngle = osuCurrObj.Angle.Value;
double lastAngle = osuLastObj.Angle.Value;
From e04727afb13d5478608987e1080270a54bee66ed Mon Sep 17 00:00:00 2001
From: Jay Lawton
Date: Mon, 20 Jan 2025 07:55:34 +1000
Subject: [PATCH 034/262] Improve convert considerations in osu!taiko (#31546)
* return a higher finger count
* implement isConvert
* diffcalc cleanup
* harshen monostaminafactor accuracy curve
* readd comment
* adjusts tests
---
.../TaikoDifficultyCalculatorTest.cs | 8 ++---
.../Difficulty/Evaluators/StaminaEvaluator.cs | 2 +-
.../Difficulty/Skills/Stamina.cs | 7 +++--
.../Difficulty/TaikoDifficultyCalculator.cs | 31 ++++++-------------
.../Difficulty/TaikoPerformanceCalculator.cs | 2 +-
5 files changed, 21 insertions(+), 29 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index d760b9aef6..6f5c26816f 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(3.3167800835687551d, 200, "diffcalc-test")]
- [TestCase(3.3167800835687551d, 200, "diffcalc-test-strong")]
+ [TestCase(3.3056113401782845d, 200, "diffcalc-test")]
+ [TestCase(3.3056113401782845d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(4.4631326105105122d, 200, "diffcalc-test")]
- [TestCase(4.4631326105105122d, 200, "diffcalc-test-strong")]
+ [TestCase(4.4473902679506896d, 200, "diffcalc-test")]
+ [TestCase(4.4473902679506896d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
index a273d91a38..b39ad953a4 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
return 2;
}
- return 4;
+ return 8;
}
///
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
index 29f9f16033..aea491aca3 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
@@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
private double strainDecayBase => 0.4;
private readonly bool singleColourStamina;
+ private readonly bool isConvert;
private double currentStrain;
@@ -28,10 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
/// Mods for use in skill calculations.
/// Reads when Stamina is from a single coloured pattern.
- public Stamina(Mod[] mods, bool singleColourStamina)
+ /// Determines if the currently evaluated beatmap is converted.
+ public Stamina(Mod[] mods, bool singleColourStamina, bool isConvert)
: base(mods)
{
this.singleColourStamina = singleColourStamina;
+ this.isConvert = isConvert;
}
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
@@ -45,7 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
var currentObject = current as TaikoDifficultyHitObject;
int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0;
- double monolengthBonus = 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30);
+ double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30);
if (singleColourStamina)
return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain);
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index 3ad9d17526..efd3001764 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private double strainLengthBonus;
private double patternMultiplier;
+ private bool isConvert;
+
public override int Version => 20241007;
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
@@ -44,13 +46,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
HitWindows hitWindows = new HitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
+ isConvert = beatmap.BeatmapInfo.Ruleset.OnlineID == 0;
+
return new Skill[]
{
new Rhythm(mods, hitWindows.WindowFor(HitResult.Great) / clockRate),
new Reading(mods),
new Colour(mods),
- new Stamina(mods, false),
- new Stamina(mods, true)
+ new Stamina(mods, false, isConvert),
+ new Stamina(mods, true, isConvert)
};
}
@@ -130,19 +134,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
+ Math.Min(Math.Max((staminaDifficultStrains - 1350) / 5000, 0), 0.15)
+ Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05);
- double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax);
+ double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert);
double starRating = rescale(combinedRating * 1.4);
- // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope.
- if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0)
- {
- starRating *= 0.7;
-
- // For maps with relax, multiple inputs are more likely to be abused.
- if (isRelax)
- starRating *= 0.60;
- }
-
HitWindows hitWindows = new TaikoHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
@@ -173,7 +167,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 combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax)
+ private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax, bool isConvert)
{
List peaks = new List();
@@ -186,14 +180,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier * patternMultiplier;
double readingPeak = readingPeaks[i] * reading_skill_multiplier;
- double colourPeak = colourPeaks[i] * colour_skill_multiplier;
+ double colourPeak = isRelax ? 0 : colourPeaks[i] * colour_skill_multiplier; // There is no colour difficulty in relax.
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * strainLengthBonus;
-
- if (isRelax)
- {
- colourPeak = 0; // There is no colour difficulty in relax.
- staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count.
- }
+ staminaPeak /= isConvert || isRelax ? 1.5 : 1.0; // Available finger count is increased by 150%, thus we adjust accordingly.
double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak, readingPeak);
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index 9e7bf7cb7a..bcd3693119 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
// Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps.
double accScalingExponent = 2 + attributes.MonoStaminaFactor;
- double accScalingShift = 500 - 100 * attributes.MonoStaminaFactor;
+ double accScalingShift = 500 - 100 * (attributes.MonoStaminaFactor * 3);
return difficultyValue * Math.Pow(DifficultyCalculationUtils.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent);
}
From 2d0bc6cb62bd9fe84b7fffb8019ff2e503a6ffc1 Mon Sep 17 00:00:00 2001
From: Jay Lawton
Date: Mon, 20 Jan 2025 08:40:09 +1000
Subject: [PATCH 035/262] Rebalance stamina length bonus in osu!taiko (#31556)
* adjust straincount to assume 1300
* remove comment
---------
Co-authored-by: StanR
---
.../Difficulty/TaikoDifficultyCalculator.cs | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index efd3001764..b1dcf2d7a0 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -124,14 +124,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double colourDifficultStrains = colour.CountTopWeightedStrains();
double rhythmDifficultStrains = rhythm.CountTopWeightedStrains();
- // Due to constraints of strain in cases where difficult strain values don't shift with range changes, we manually apply clockrate.
- double staminaDifficultStrains = stamina.CountTopWeightedStrains() * clockRate;
+ double staminaDifficultStrains = stamina.CountTopWeightedStrains();
// As we don't have pattern integration in osu!taiko, we apply the other two skills relative to rhythm.
patternMultiplier = Math.Pow(staminaRating * colourRating, 0.10);
strainLengthBonus = 1
- + Math.Min(Math.Max((staminaDifficultStrains - 1350) / 5000, 0), 0.15)
+ + Math.Min(Math.Max((staminaDifficultStrains - 1000) / 3700, 0), 0.15)
+ Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05);
double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert);
From e57565435ed58fc4e549559350886df1fa4d4189 Mon Sep 17 00:00:00 2001
From: Eloise
Date: Mon, 20 Jan 2025 08:40:52 +0000
Subject: [PATCH 036/262] osu!taiko new rhythm penalty for long intervals using
stamina difficulty (#31573)
* Replace long interval nerf with a new one that uses stamina difficulty
* Turn tabs into spaces
* Update unit tests
---------
Co-authored-by: StanR
---
.../TaikoDifficultyCalculatorTest.cs | 8 ++++----
osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs | 3 ++-
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index 6f5c26816f..76b86eb4d6 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(3.3056113401782845d, 200, "diffcalc-test")]
- [TestCase(3.3056113401782845d, 200, "diffcalc-test-strong")]
+ [TestCase(3.305554470092722d, 200, "diffcalc-test")]
+ [TestCase(3.305554470092722d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(4.4473902679506896d, 200, "diffcalc-test")]
- [TestCase(4.4473902679506896d, 200, "diffcalc-test-strong")]
+ [TestCase(4.4472572672057815d, 200, "diffcalc-test")]
+ [TestCase(4.4472572672057815d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
index 4fe1ea693e..45d0d0a548 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
@@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
double difficulty = RhythmEvaluator.EvaluateDifficultyOf(current, greatHitWindow);
// To prevent abuse of exceedingly long intervals between awkward rhythms, we penalise its difficulty.
- difficulty *= DifficultyCalculationUtils.Logistic(current.DeltaTime, 350, -1 / 25.0, 0.5) + 0.5;
+ double staminaDifficulty = StaminaEvaluator.EvaluateDifficultyOf(current) - 0.5; // Remove base strain
+ difficulty *= DifficultyCalculationUtils.Logistic(staminaDifficulty, 1 / 15.0, 50.0);
return difficulty;
}
From 22e839d62b646f6f42b129df83336694547bef8e Mon Sep 17 00:00:00 2001
From: StanR
Date: Mon, 20 Jan 2025 14:39:35 +0500
Subject: [PATCH 037/262] Replace indexed skill access with
`skills.OfType<...>().Single()` (#30034)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Replace indexed skill access with `skills.First(s is ...)`
* Fix comment
* Further refactoring to remove casts
---------
Co-authored-by: Dan Balasescu
Co-authored-by: Bartłomiej Dach
---
.../Difficulty/CatchDifficultyCalculator.cs | 3 ++-
.../Difficulty/ManiaDifficultyCalculator.cs | 2 +-
.../Difficulty/OsuDifficultyCalculator.cs | 24 ++++++++++---------
.../Difficulty/Skills/Aim.cs | 10 ++++----
.../Difficulty/Skills/Stamina.cs | 8 +++----
.../Difficulty/TaikoDifficultyCalculator.cs | 10 ++++----
6 files changed, 30 insertions(+), 27 deletions(-)
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index 7d21409ee8..99df2731ff 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
@@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
{
- StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier,
+ StarRating = Math.Sqrt(skills.OfType().Single().DifficultyValue()) * difficulty_multiplier,
Mods = mods,
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
MaxCombo = beatmap.GetMaxCombo(),
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index ff9aa4aa7b..1efa7cb42f 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes
{
- StarRating = skills[0].DifficultyValue() * difficulty_multiplier,
+ StarRating = skills.OfType().Single().DifficultyValue() * difficulty_multiplier,
Mods = mods,
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 5a61ea586a..1505c51592 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -36,20 +36,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (beatmap.HitObjects.Count == 0)
return new OsuDifficultyAttributes { Mods = mods };
- double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
- double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
- double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
- double speedNotes = ((Speed)skills[2]).RelevantNoteCount();
- double difficultSliders = ((Aim)skills[0]).GetDifficultSliders();
- double flashlightRating = 0.0;
-
- if (mods.Any(h => h is OsuModFlashlight))
- flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
+ var aim = skills.OfType().Single(a => a.IncludeSliders);
+ double aimRating = Math.Sqrt(aim.DifficultyValue()) * difficulty_multiplier;
+ double aimDifficultyStrainCount = aim.CountTopWeightedStrains();
+ double difficultSliders = aim.GetDifficultSliders();
+ var aimWithoutSliders = skills.OfType().Single(a => !a.IncludeSliders);
+ double aimRatingNoSliders = Math.Sqrt(aimWithoutSliders.DifficultyValue()) * difficulty_multiplier;
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
- double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountTopWeightedStrains();
- double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountTopWeightedStrains();
+ var speed = skills.OfType().Single();
+ double speedRating = Math.Sqrt(speed.DifficultyValue()) * difficulty_multiplier;
+ double speedNotes = speed.RelevantNoteCount();
+ double speedDifficultyStrainCount = speed.CountTopWeightedStrains();
+
+ var flashlight = skills.OfType().SingleOrDefault();
+ double flashlightRating = flashlight == null ? 0.0 : Math.Sqrt(flashlight.DifficultyValue()) * difficulty_multiplier;
if (mods.Any(m => m is OsuModTouchDevice))
{
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index f04b679b73..89adda302c 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -16,14 +16,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
public class Aim : OsuStrainSkill
{
- public Aim(Mod[] mods, bool withSliders)
+ public readonly bool IncludeSliders;
+
+ public Aim(Mod[] mods, bool includeSliders)
: base(mods)
{
- this.withSliders = withSliders;
+ IncludeSliders = includeSliders;
}
- private readonly bool withSliders;
-
private double currentStrain;
private double skillMultiplier => 25.6;
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override double StrainValueAt(DifficultyHitObject current)
{
currentStrain *= strainDecay(current.DeltaTime);
- currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier;
+ currentStrain += AimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplier;
if (current.BaseObject is Slider)
{
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
index aea491aca3..12e1396dd7 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
private double skillMultiplier => 1.1;
private double strainDecayBase => 0.4;
- private readonly bool singleColourStamina;
+ public readonly bool SingleColourStamina;
private readonly bool isConvert;
private double currentStrain;
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
public Stamina(Mod[] mods, bool singleColourStamina, bool isConvert)
: base(mods)
{
- this.singleColourStamina = singleColourStamina;
+ SingleColourStamina = singleColourStamina;
this.isConvert = isConvert;
}
@@ -50,12 +50,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30);
- if (singleColourStamina)
+ if (SingleColourStamina)
return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain);
return currentStrain * monolengthBonus;
}
- protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => singleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime);
+ protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => SingleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime);
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index b1dcf2d7a0..bcd26a06bc 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -109,11 +109,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
bool isRelax = mods.Any(h => h is TaikoModRelax);
- Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
- Reading reading = (Reading)skills.First(x => x is Reading);
- Colour colour = (Colour)skills.First(x => x is Colour);
- Stamina stamina = (Stamina)skills.First(x => x is Stamina);
- Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina);
+ var rhythm = skills.OfType().Single();
+ var reading = skills.OfType().Single();
+ var colour = skills.OfType().Single();
+ var stamina = skills.OfType().Single(s => !s.SingleColourStamina);
+ var singleColourStamina = skills.OfType().Single(s => s.SingleColourStamina);
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
double readingRating = reading.DifficultyValue() * reading_skill_multiplier;
From a77dfb106834e8818574e81b1d7880d38c0e929b Mon Sep 17 00:00:00 2001
From: James Wilson
Date: Mon, 20 Jan 2025 12:04:31 +0000
Subject: [PATCH 038/262] Use correct `HitWindows` class for osu!taiko hit
windows in difficulty calculator (#31579)
* Use correct `HitWindows` class for osu!taiko hit windows in difficulty calculator
* Remove redundant (and incorrect) hit window creation
* Balance rhythm against hit window changes
---
.../Difficulty/Evaluators/RhythmEvaluator.cs | 2 +-
.../Difficulty/TaikoDifficultyCalculator.cs | 5 +----
2 files changed, 2 insertions(+), 5 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
index 7d58eada5e..e7d82453eb 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
@@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
intervalDifficulty *= DifficultyCalculationUtils.Logistic(
durationDifference / hitWindow,
midpointOffset: 0.7,
- multiplier: 1.5,
+ multiplier: 1.0,
maxValue: 1);
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index bcd26a06bc..f3b976f970 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{
- HitWindows hitWindows = new HitWindows();
+ HitWindows hitWindows = new TaikoHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
isConvert = beatmap.BeatmapInfo.Ruleset.OnlineID == 0;
@@ -68,9 +68,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
- var hitWindows = new HitWindows();
- hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
-
var difficultyHitObjects = new List();
var centreObjects = new List();
var rimObjects = new List();
From c8b05ce114a00e9123ba5b3ac8930f1fafde88a9 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Tue, 21 Jan 2025 13:40:55 +0900
Subject: [PATCH 039/262] Tidy up code quality of `RhythmEvaluator`
---
.../Difficulty/Evaluators/RhythmEvaluator.cs | 149 ++++++++----------
1 file changed, 68 insertions(+), 81 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
index e7d82453eb..22321a8f6e 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
@@ -14,48 +14,64 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
public class RhythmEvaluator
{
///
- /// Multiplier for a given denominator term.
+ /// Evaluate the difficulty of a hitobject considering its interval change.
///
- private static double termPenalty(double ratio, int denominator, double power, double multiplier)
+ public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow)
{
- return -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power);
- }
+ TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm;
+ double difficulty = 0.0d;
- ///
- /// Validates the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions.
- ///
- private static double validateRatio(double ratio)
- {
- return double.IsNormal(ratio) ? ratio : 0;
- }
+ double sameRhythm = 0;
+ double samePattern = 0;
+ double intervalPenalty = 0;
- ///
- /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses.
- ///
- private static double ratioDifficulty(double ratio, int terms = 8)
- {
- double difficulty = 0;
- ratio = validateRatio(ratio);
-
- for (int i = 1; i <= terms; ++i)
+ if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects
{
- difficulty += termPenalty(ratio, i, 4, 1);
+ sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow);
+ intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow);
}
- difficulty += terms / (1 + ratio);
+ if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns
+ samePattern += 1.15 * ratioDifficulty(rhythm.SamePatterns.IntervalRatio);
- // Give bonus to near-1 ratios
- difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5);
-
- // Penalize ratios that are VERY near 1
- difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3);
-
- difficulty = Math.Max(difficulty, 0);
- difficulty /= Math.Sqrt(8);
+ difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty;
return difficulty;
}
+ private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow)
+ {
+ double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio);
+ double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval;
+
+ intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow);
+
+ // If a previous interval exists and there are multiple hit objects in the sequence:
+ if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1)
+ {
+ double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count;
+ double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious;
+
+ if (durationDifference > 0)
+ {
+ intervalDifficulty *= DifficultyCalculationUtils.Logistic(
+ durationDifference / hitWindow,
+ midpointOffset: 0.7,
+ multiplier: 1.0,
+ maxValue: 1);
+ }
+ }
+
+ // Penalise patterns that can be hit within a single hit window.
+ intervalDifficulty *= DifficultyCalculationUtils.Logistic(
+ sameRhythmHitObjects.Duration / hitWindow,
+ midpointOffset: 0.6,
+ multiplier: 1,
+ maxValue: 1);
+
+ return Math.Pow(intervalDifficulty, 0.75);
+ }
+
///
/// Determines if the changes in hit object intervals is consistent based on a given threshold.
///
@@ -102,68 +118,39 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
}
}
- private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow)
- {
- double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio);
- double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval;
-
- intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow);
-
- // If a previous interval exists and there are multiple hit objects in the sequence:
- if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1)
- {
- double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count;
- double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious;
-
- if (durationDifference > 0)
- {
- intervalDifficulty *= DifficultyCalculationUtils.Logistic(
- durationDifference / hitWindow,
- midpointOffset: 0.7,
- multiplier: 1.0,
- maxValue: 1);
- }
- }
-
- // Penalise patterns that can be hit within a single hit window.
- intervalDifficulty *= DifficultyCalculationUtils.Logistic(
- sameRhythmHitObjects.Duration / hitWindow,
- midpointOffset: 0.6,
- multiplier: 1,
- maxValue: 1);
-
- return Math.Pow(intervalDifficulty, 0.75);
- }
-
- private static double evaluateDifficultyOf(SamePatterns samePatterns)
- {
- return ratioDifficulty(samePatterns.IntervalRatio);
- }
-
///
- /// Evaluate the difficulty of a hitobject considering its interval change.
+ /// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses.
///
- public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow)
+ private static double ratioDifficulty(double ratio, int terms = 8)
{
- TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm;
- double difficulty = 0.0d;
+ double difficulty = 0;
- double sameRhythm = 0;
- double samePattern = 0;
- double intervalPenalty = 0;
+ // Validate the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions.
+ ratio = double.IsNormal(ratio) ? ratio : 0;
- if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects
+ for (int i = 1; i <= terms; ++i)
{
- sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow);
- intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow);
+ difficulty += termPenalty(ratio, i, 4, 1);
}
- if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns
- samePattern += 1.15 * evaluateDifficultyOf(rhythm.SamePatterns);
+ difficulty += terms / (1 + ratio);
- difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty;
+ // Give bonus to near-1 ratios
+ difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5);
+
+ // Penalize ratios that are VERY near 1
+ difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3);
+
+ difficulty = Math.Max(difficulty, 0);
+ difficulty /= Math.Sqrt(8);
return difficulty;
}
+
+ ///
+ /// Multiplier for a given denominator term.
+ ///
+ private static double termPenalty(double ratio, int denominator, double power, double multiplier) =>
+ -multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power);
}
}
From 001d9cacf21cbe9dee9330b01b9e496e7be1f4f5 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Tue, 21 Jan 2025 19:31:49 +0900
Subject: [PATCH 040/262] Configure awaiters
---
osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++--
osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index d0c3a1fa06..e5eade8c1d 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -171,7 +171,7 @@ namespace osu.Game.Online.Multiplayer
throw new InvalidOperationException("Cannot join a multiplayer room while already in one.");
var cancellationSource = joinCancellationSource = new CancellationTokenSource();
- await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token);
+ await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false);
}
///
@@ -187,7 +187,7 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(room.RoomID != null);
var cancellationSource = joinCancellationSource = new CancellationTokenSource();
- await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token);
+ await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false);
}
private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken)
diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
index 524873ef66..05f3e44405 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -275,7 +275,7 @@ namespace osu.Game.Online.Multiplayer
try
{
- return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room);
+ return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room).ConfigureAwait(false);
}
catch (HubException exception)
{
From fa20bc6631b084b4fbd3b97c3cd257a005379b0e Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Tue, 21 Jan 2025 14:24:04 +0000
Subject: [PATCH 041/262] Remove `EffectiveBPMPreprocessor`
---
.../Preprocessing/Reading/EffectiveBPM.cs | 50 -------------------
.../Preprocessing/TaikoDifficultyHitObject.cs | 27 +++++++++-
.../Difficulty/TaikoDifficultyCalculator.cs | 13 +++--
3 files changed, 32 insertions(+), 58 deletions(-)
delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs
deleted file mode 100644
index 17e05d5fbf..0000000000
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Reading/EffectiveBPM.cs
+++ /dev/null
@@ -1,50 +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.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
-
-namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading
-{
- public class EffectiveBPMPreprocessor
- {
- private readonly IList noteObjects;
- private readonly double globalSliderVelocity;
-
- public EffectiveBPMPreprocessor(IBeatmap beatmap, List noteObjects)
- {
- this.noteObjects = noteObjects;
- globalSliderVelocity = beatmap.Difficulty.SliderMultiplier;
- }
-
- ///
- /// Calculates and sets the effective BPM and slider velocity for each note object, considering clock rate and scroll speed.
- ///
- public void ProcessEffectiveBPM(ControlPointInfo controlPointInfo, double clockRate)
- {
- foreach (var currentNoteObject in noteObjects)
- {
- double startTime = currentNoteObject.StartTime * clockRate;
-
- // Retrieve the timing point at the note's start time
- TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime);
-
- // Calculate the slider velocity at the note's start time.
- double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, startTime, clockRate);
- currentNoteObject.CurrentSliderVelocity = currentSliderVelocity;
-
- currentNoteObject.EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity;
- }
- }
-
- ///
- /// Calculates the slider velocity based on control point info and clock rate.
- ///
- private double calculateSliderVelocity(ControlPointInfo controlPointInfo, double startTime, double clockRate)
- {
- var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime);
- return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
index dfcd08ed94..34c4871a42 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Objects;
@@ -76,11 +77,15 @@ 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 drumroll or swell) in the current beatmap.
/// The position of this in the list.
+ /// The control point info of the beatmap.
+ /// The global slider velocity of the beatmap.
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate,
List objects,
List centreHitObjects,
List rimHitObjects,
- List noteObjects, int index)
+ List noteObjects, int index,
+ ControlPointInfo controlPointInfo,
+ double globalSliderVelocity)
: base(hitObject, lastObject, clockRate, objects, index)
{
noteDifficultyHitObjects = noteObjects;
@@ -111,6 +116,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
NoteIndex = noteObjects.Count;
noteObjects.Add(this);
}
+
+ double startTime = hitObject.StartTime * clockRate;
+
+ // Retrieve the timing point at the note's start time
+ TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime);
+
+ // Calculate the slider velocity at the note's start time.
+ double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, startTime, clockRate);
+ CurrentSliderVelocity = currentSliderVelocity;
+
+ EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity;
+ }
+
+ ///
+ /// Calculates the slider velocity based on control point info and clock rate.
+ ///
+ private static double calculateSliderVelocity(ControlPointInfo controlPointInfo, double globalSliderVelocity, double startTime, double clockRate)
+ {
+ var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime);
+ return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate;
}
public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1));
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index f3b976f970..1d3075e4ac 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -13,7 +13,6 @@ 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.Preprocessing.Reading;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
using osu.Game.Rulesets.Taiko.Mods;
@@ -72,7 +71,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
var centreObjects = new List();
var rimObjects = new List();
var noteObjects = new List();
- EffectiveBPMPreprocessor bpmLoader = new EffectiveBPMPreprocessor(beatmap, noteObjects);
// Generate TaikoDifficultyHitObjects from the beatmap's hit objects.
for (int i = 2; i < beatmap.HitObjects.Count; i++)
@@ -86,15 +84,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
centreObjects,
rimObjects,
noteObjects,
- difficultyHitObjects.Count
+ difficultyHitObjects.Count,
+ beatmap.ControlPointInfo,
+ beatmap.Difficulty.SliderMultiplier
));
}
- var groupedHitObjects = SameRhythmHitObjects.GroupHitObjects(noteObjects);
-
TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects);
- SamePatterns.GroupPatterns(groupedHitObjects);
- bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate);
+
+ var groupedHitObjects = SameRhythmGroupedHitObjects.GroupHitObjects(noteObjects);
+ SamePatternsGroupedHitObjects.GroupPatterns(groupedHitObjects);
return difficultyHitObjects;
}
From dbe36887f6da2649e9c55e265d6e4eb15429929a Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Tue, 21 Jan 2025 14:24:27 +0000
Subject: [PATCH 042/262] Refactor `ColourEvaluator`
---
.../Difficulty/Evaluators/ColourEvaluator.cs | 41 ++++++-------------
1 file changed, 13 insertions(+), 28 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
index 3ff5b87fb6..c0e90e83c1 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
@@ -10,32 +10,8 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{
- public class ColourEvaluator
+ public static class ColourEvaluator
{
- ///
- /// Evaluate the difficulty of the first note of a .
- ///
- public static double EvaluateDifficultyOf(MonoStreak monoStreak)
- {
- return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
- }
-
- ///
- /// Evaluate the difficulty of the first note of a .
- ///
- public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern)
- {
- return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
- }
-
- ///
- /// Evaluate the difficulty of the first note of a .
- ///
- public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern)
- {
- return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
- }
-
///
/// Calculates a consistency penalty based on the number of consecutive consistent intervals,
/// considering the delta time between each colour sequence.
@@ -89,18 +65,27 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
double difficulty = 0.0d;
if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
- difficulty += EvaluateDifficultyOf(colour.MonoStreak);
+ difficulty += evaluateMonoStreakDifficulty(colour.MonoStreak);
if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
- difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
+ difficulty += evaluateAlternatingMonoPatternDifficulty(colour.AlternatingMonoPattern);
if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
- difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);
+ difficulty += evaluateReadingHitPatternDifficulty(colour.RepeatingHitPattern);
double consistencyPenalty = consistentRatioPenalty(taikoObject);
difficulty *= consistencyPenalty;
return difficulty;
}
+
+ private static double evaluateMonoStreakDifficulty(MonoStreak monoStreak) =>
+ DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * evaluateAlternatingMonoPatternDifficulty(monoStreak.Parent) * 0.5;
+
+ private static double evaluateAlternatingMonoPatternDifficulty(AlternatingMonoPattern alternatingMonoPattern) =>
+ DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateReadingHitPatternDifficulty(alternatingMonoPattern.Parent);
+
+ private static double evaluateReadingHitPatternDifficulty(RepeatingHitPatterns repeatingHitPattern) =>
+ 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
}
}
From 9919179b0b914aba42499467cba38ee2d311034b Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Tue, 21 Jan 2025 14:24:46 +0000
Subject: [PATCH 043/262] Format `ReadingEvaluator`
---
.../Difficulty/Evaluators/ReadingEvaluator.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs
index 2a08f65c7b..5871979613 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ReadingEvaluator.cs
@@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
// High density is penalised at high velocity as it is generally considered easier to read. See https://www.desmos.com/calculator/u63f3ntdsi
double densityPenalty = DifficultyCalculationUtils.Logistic(objectDensity, 0.925, 15);
- double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty) * DifficultyCalculationUtils.Logistic
- (effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10));
+ double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty)
+ * DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10));
return midVelocityDifficulty + highVelocityDifficulty;
}
From b8c79d58a731943f46433298db8eb0523ec850b7 Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Tue, 21 Jan 2025 14:25:28 +0000
Subject: [PATCH 044/262] Refactor `StaminaEvaluator`
---
.../Difficulty/Evaluators/StaminaEvaluator.cs | 54 +++++++++----------
1 file changed, 27 insertions(+), 27 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
index b39ad953a4..a9884b2328 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
@@ -8,8 +8,34 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{
- public class StaminaEvaluator
+ public static class StaminaEvaluator
{
+ ///
+ /// 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 available fingers for each colour.
+ ///
+ public static double EvaluateDifficultyOf(DifficultyHitObject current)
+ {
+ if (current.BaseObject is not Hit)
+ {
+ return 0.0;
+ }
+
+ // Find the previous hit object hit by the current finger, which is n notes prior, n being the number of
+ // available fingers.
+ TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current;
+ TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject;
+ TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);
+
+ double objectStrain = 0.5; // Add a base strain to all objects
+ if (taikoPrevious == null) return objectStrain;
+
+ if (previousMono != null)
+ objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime);
+
+ return objectStrain;
+ }
+
///
/// Applies a speed bonus dependent on the time since the last hit performed using this finger.
///
@@ -44,31 +70,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
return 8;
}
-
- ///
- /// 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 available fingers for each colour.
- ///
- public static double EvaluateDifficultyOf(DifficultyHitObject current)
- {
- if (current.BaseObject is not Hit)
- {
- return 0.0;
- }
-
- // Find the previous hit object hit by the current finger, which is n notes prior, n being the number of
- // available fingers.
- TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current;
- TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject;
- TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);
-
- double objectStrain = 0.5; // Add a base strain to all objects
- if (taikoPrevious == null) return objectStrain;
-
- if (previousMono != null)
- objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime);
-
- return objectStrain;
- }
}
}
From ef8867704adaeb813bce65fe1e44844aea86ddce Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Tue, 21 Jan 2025 14:28:15 +0000
Subject: [PATCH 045/262] Add xmldoc to explain `IHasInterval.Interval`
---
.../Difficulty/Preprocessing/Rhythm/IHasInterval.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs
index 8f3917cbde..32b148da2e 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs
@@ -8,6 +8,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
///
public interface IHasInterval
{
+ ///
+ /// The interval between 2 objects start times.
+ ///
double Interval { get; }
}
}
From 20a76d832df7986c623f9e7fecd468fc012782eb Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Tue, 21 Jan 2025 14:29:07 +0000
Subject: [PATCH 046/262] Rename rhythm preprocessing objects to be clearer
with intent
---
.../Difficulty/Evaluators/RhythmEvaluator.cs | 38 +++++++++----------
...Rhythm.cs => IntervalGroupedHitObjects.cs} | 31 ++++++---------
...ns.cs => SamePatternsGroupedHitObjects.cs} | 28 +++++++-------
...ects.cs => SameRhythmGroupedHitObjects.cs} | 30 +++++++--------
.../Rhythm/TaikoDifficultyHitObjectRhythm.cs | 4 +-
5 files changed, 60 insertions(+), 71 deletions(-)
rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SameRhythm.cs => IntervalGroupedHitObjects.cs} (62%)
rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SamePatterns.cs => SamePatternsGroupedHitObjects.cs} (50%)
rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SameRhythmHitObjects.cs => SameRhythmGroupedHitObjects.cs} (70%)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
index 22321a8f6e..8accc6124c 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
@@ -25,32 +25,32 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
double samePattern = 0;
double intervalPenalty = 0;
- if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects
+ if (rhythm.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects
{
- sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow);
- intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow);
+ sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmGroupedHitObjects, hitWindow);
+ intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmGroupedHitObjects, hitWindow);
}
- if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns
- samePattern += 1.15 * ratioDifficulty(rhythm.SamePatterns.IntervalRatio);
+ if (rhythm.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects
+ samePattern += 1.15 * ratioDifficulty(rhythm.SamePatternsGroupedHitObjects.IntervalRatio);
difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty;
return difficulty;
}
- private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow)
+ private static double evaluateDifficultyOf(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow)
{
- double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio);
- double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval;
+ double intervalDifficulty = ratioDifficulty(sameRhythmGroupedHitObjects.HitObjectIntervalRatio);
+ double? previousInterval = sameRhythmGroupedHitObjects.Previous?.HitObjectInterval;
- intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow);
+ intervalDifficulty *= repeatedIntervalPenalty(sameRhythmGroupedHitObjects, hitWindow);
// If a previous interval exists and there are multiple hit objects in the sequence:
- if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1)
+ if (previousInterval != null && sameRhythmGroupedHitObjects.Children.Count > 1)
{
- double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count;
- double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious;
+ double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.Children.Count;
+ double durationDifference = sameRhythmGroupedHitObjects.Duration - expectedDurationFromPrevious;
if (durationDifference > 0)
{
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
// Penalise patterns that can be hit within a single hit window.
intervalDifficulty *= DifficultyCalculationUtils.Logistic(
- sameRhythmHitObjects.Duration / hitWindow,
+ sameRhythmGroupedHitObjects.Duration / hitWindow,
midpointOffset: 0.6,
multiplier: 1,
maxValue: 1);
@@ -75,20 +75,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
///
/// Determines if the changes in hit object intervals is consistent based on a given threshold.
///
- private static double repeatedIntervalPenalty(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow, double threshold = 0.1)
+ private static double repeatedIntervalPenalty(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1)
{
- double longIntervalPenalty = sameInterval(sameRhythmHitObjects, 3);
+ double longIntervalPenalty = sameInterval(sameRhythmGroupedHitObjects, 3);
- double shortIntervalPenalty = sameRhythmHitObjects.Children.Count < 6
- ? sameInterval(sameRhythmHitObjects, 4)
+ double shortIntervalPenalty = sameRhythmGroupedHitObjects.Children.Count < 6
+ ? sameInterval(sameRhythmGroupedHitObjects, 4)
: 1.0; // Returns a non-penalty if there are 6 or more notes within an interval.
// The duration penalty is based on hit object duration relative to hitWindow.
- double durationPenalty = Math.Max(1 - sameRhythmHitObjects.Duration * 2 / hitWindow, 0.5);
+ double durationPenalty = Math.Max(1 - sameRhythmGroupedHitObjects.Duration * 2 / hitWindow, 0.5);
return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty;
- double sameInterval(SameRhythmHitObjects startObject, int intervalCount)
+ double sameInterval(SameRhythmGroupedHitObjects startObject, int intervalCount)
{
List intervals = new List();
var currentObject = startObject;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs
similarity index 62%
rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs
rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs
index b1ca22595b..930b3fc0e4 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs
@@ -1,8 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
+using osu.Framework.Utils;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
{
@@ -10,35 +10,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
/// A base class for grouping s by their interval. In edges where an interval change
/// occurs, the is added to the group with the smaller interval.
///
- public abstract class SameRhythm
- where ChildType : IHasInterval
+ public abstract class IntervalGroupedHitObjects
+ where TChildType : IHasInterval
{
- public IReadOnlyList Children { get; private set; }
+ public IReadOnlyList Children { get; private set; }
///
- /// Determines if the intervals between two child objects are within a specified margin of error,
- /// indicating that the intervals are effectively "flat" or consistent.
- ///
- private bool isFlat(ChildType current, ChildType previous, double marginOfError)
- {
- return Math.Abs(current.Interval - previous.Interval) <= marginOfError;
- }
-
- ///
- /// Create a new from a list of s, and add
+ /// Create a new from a list of s, and add
/// them to the list until the end of the group.
///
/// The list of s.
///
/// Index in to start adding children. This will be modified and should be passed into
- /// the next 's constructor.
+ /// the next 's constructor.
///
///
/// The margin of error for the interval, within of which no interval change is considered to have occured.
///
- protected SameRhythm(List data, ref int i, double marginOfError)
+ protected IntervalGroupedHitObjects(List data, ref int i, double marginOfError)
{
- List children = new List();
+ List children = new List();
Children = children;
children.Add(data[i]);
i++;
@@ -46,9 +37,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
for (; i < data.Count - 1; i++)
{
// An interval change occured, add the current data if the next interval is larger.
- if (!isFlat(data[i], data[i + 1], marginOfError))
+ if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError))
{
- if (data[i + 1].Interval > data[i].Interval + marginOfError)
+ if (Precision.DefinitelyBigger(data[i].Interval, data[i + 1].Interval, marginOfError))
{
children.Add(data[i]);
i++;
@@ -63,7 +54,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
// Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error.
// If true, add the current object to the group and increment the index to process the next object.
- if (data.Count > 2 && isFlat(data[^1], data[^2], marginOfError))
+ if (data.Count > 2 && Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError))
{
children.Add(data[i]);
i++;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs
similarity index 50%
rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs
rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs
index 50839c4561..d4cbc9c1f9 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatterns.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs
@@ -7,21 +7,21 @@ using System.Linq;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
{
///
- /// Represents grouped by their 's interval.
+ /// Represents grouped by their 's interval.
///
- public class SamePatterns : SameRhythm
+ public class SamePatternsGroupedHitObjects : IntervalGroupedHitObjects
{
- public SamePatterns? Previous { get; private set; }
+ public SamePatternsGroupedHitObjects? Previous { get; private set; }
///
- /// The between children within this group.
- /// If there is only one child, this will have the value of the first child's .
+ /// The between children within this group.
+ /// If there is only one child, this will have the value of the first child's .
///
public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval;
///
- /// The ratio of between this and the previous . In the
- /// case where there is no previous , this will have a value of 1.
+ /// The ratio of between this and the previous . In the
+ /// case where there is no previous , this will have a value of 1.
///
public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d;
@@ -29,26 +29,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children);
- private SamePatterns(SamePatterns? previous, List data, ref int i)
+ private SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List data, ref int i)
: base(data, ref i, 5)
{
Previous = previous;
foreach (TaikoDifficultyHitObject hitObject in AllHitObjects)
{
- hitObject.Rhythm.SamePatterns = this;
+ hitObject.Rhythm.SamePatternsGroupedHitObjects = this;
}
}
- public static void GroupPatterns(List data)
+ public static void GroupPatterns(List data)
{
- List samePatterns = new List();
+ List samePatterns = new List();
- // Index does not need to be incremented, as it is handled within the SameRhythm constructor.
+ // Index does not need to be incremented, as it is handled within the IntervalGroupedHitObjects constructor.
for (int i = 0; i < data.Count;)
{
- SamePatterns? previous = samePatterns.Count > 0 ? samePatterns[^1] : null;
- samePatterns.Add(new SamePatterns(previous, data, ref i));
+ SamePatternsGroupedHitObjects? previous = samePatterns.Count > 0 ? samePatterns[^1] : null;
+ samePatterns.Add(new SamePatternsGroupedHitObjects(previous, data, ref i));
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs
similarity index 70%
rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs
rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs
index 0ccc6da026..0b59433a2e 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjects.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs
@@ -9,11 +9,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
///
/// Represents a group of s with no rhythm variation.
///
- public class SameRhythmHitObjects : SameRhythm, IHasInterval
+ public class SameRhythmGroupedHitObjects : IntervalGroupedHitObjects, IHasInterval
{
public TaikoDifficultyHitObject FirstHitObject => Children[0];
- public SameRhythmHitObjects? Previous;
+ public SameRhythmGroupedHitObjects? Previous;
///
/// of the first hit object.
@@ -26,30 +26,28 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
public double Duration => Children[^1].StartTime - Children[0].StartTime;
///
- /// The interval in ms of each hit object in this . This is only defined if there is
- /// more than two hit objects in this .
+ /// The interval in ms of each hit object in this . This is only defined if there is
+ /// more than two hit objects in this .
///
public double? HitObjectInterval;
///
- /// The ratio of between this and the previous . In the
+ /// The ratio of between this and the previous . In the
/// case where one or both of the is undefined, this will have a value of 1.
///
public double HitObjectIntervalRatio = 1;
- ///
- /// The interval between the of this and the previous .
- ///
- public double Interval { get; private set; } = double.PositiveInfinity;
+ ///
+ public double Interval { get; private set; }
- public SameRhythmHitObjects(SameRhythmHitObjects? previous, List data, ref int i)
+ public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List data, ref int i)
: base(data, ref i, 5)
{
Previous = previous;
foreach (var hitObject in Children)
{
- hitObject.Rhythm.SameRhythmHitObjects = this;
+ hitObject.Rhythm.SameRhythmGroupedHitObjects = this;
// Pass the HitObjectInterval to each child.
hitObject.HitObjectInterval = HitObjectInterval;
@@ -58,15 +56,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
calculateIntervals();
}
- public static List GroupHitObjects(List data)
+ public static List GroupHitObjects(List data)
{
- List flatPatterns = new List();
+ List flatPatterns = new List();
- // Index does not need to be incremented, as it is handled within SameRhythm's constructor.
+ // Index does not need to be incremented, as it is handled within IntervalGroupedHitObjects's constructor.
for (int i = 0; i < data.Count;)
{
- SameRhythmHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null;
- flatPatterns.Add(new SameRhythmHitObjects(previous, data, ref i));
+ SameRhythmGroupedHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null;
+ flatPatterns.Add(new SameRhythmGroupedHitObjects(previous, data, ref i));
}
return flatPatterns;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs
index beb7bfe5f6..351015ae08 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs
@@ -15,12 +15,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
///
/// The group of hit objects with consistent rhythm that this object belongs to.
///
- public SameRhythmHitObjects? SameRhythmHitObjects;
+ public SameRhythmGroupedHitObjects? SameRhythmGroupedHitObjects;
///
/// The larger pattern of rhythm groups that this object is part of.
///
- public SamePatterns? SamePatterns;
+ public SamePatternsGroupedHitObjects? SamePatternsGroupedHitObjects;
///
/// The ratio of current
From e0882d2a53d5452bb539bb9b16a0019b3f4094d2 Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Tue, 21 Jan 2025 14:33:40 +0000
Subject: [PATCH 047/262] Make `rescale` a static method
---
.../Difficulty/TaikoDifficultyCalculator.cs | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index 1d3075e4ac..e07a965ab0 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -203,9 +203,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
/// Applies a final re-scaling of the star rating.
///
/// The raw star rating value before re-scaling.
- private double rescale(double sr)
+ private static double rescale(double sr)
{
- if (sr < 0) return sr;
+ if (sr < 0)
+ return sr;
return 10.43 * Math.Log(sr / 8 + 1);
}
From 764b0001efc8ec7bc9aff48c525ee78f47b468aa Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Tue, 21 Jan 2025 14:56:51 +0000
Subject: [PATCH 048/262] Fix typo in `ColourEvaluator`
---
.../Difficulty/Evaluators/ColourEvaluator.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
index c0e90e83c1..166c01f507 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
@@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
difficulty += evaluateAlternatingMonoPatternDifficulty(colour.AlternatingMonoPattern);
if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
- difficulty += evaluateReadingHitPatternDifficulty(colour.RepeatingHitPattern);
+ difficulty += evaluateRepeatingHitPatternsDifficulty(colour.RepeatingHitPattern);
double consistencyPenalty = consistentRatioPenalty(taikoObject);
difficulty *= consistencyPenalty;
@@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * evaluateAlternatingMonoPatternDifficulty(monoStreak.Parent) * 0.5;
private static double evaluateAlternatingMonoPatternDifficulty(AlternatingMonoPattern alternatingMonoPattern) =>
- DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateReadingHitPatternDifficulty(alternatingMonoPattern.Parent);
+ DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateRepeatingHitPatternsDifficulty(alternatingMonoPattern.Parent);
- private static double evaluateReadingHitPatternDifficulty(RepeatingHitPatterns repeatingHitPattern) =>
+ private static double evaluateRepeatingHitPatternsDifficulty(RepeatingHitPatterns repeatingHitPattern) =>
2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
}
}
From 1c4bc6dffd64126ab1b380ab0e6d11ff17c16a32 Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Tue, 21 Jan 2025 15:00:23 +0000
Subject: [PATCH 049/262] Revert `Precision.DefinitelyBigger` usage
---
.../Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs
index 930b3fc0e4..cc389d4091 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
// An interval change occured, add the current data if the next interval is larger.
if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError))
{
- if (Precision.DefinitelyBigger(data[i].Interval, data[i + 1].Interval, marginOfError))
+ if (data[i + 1].Interval > data[i].Interval + marginOfError)
{
children.Add(data[i]);
i++;
From 14c68bcc583d1e980225da3f022176412ede3cb8 Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Tue, 21 Jan 2025 15:58:33 +0000
Subject: [PATCH 050/262] Replace weird `IntervalGroupedHitObjects` inheritance
layer
---
.../Rhythm/Data/IntervalGroupedHitObjects.cs | 64 -------------------
.../Data/SamePatternsGroupedHitObjects.cs | 27 ++------
.../Data/SameRhythmGroupedHitObjects.cs | 57 ++++-------------
.../TaikoRhythmDifficultyPreprocessor.cs | 63 ++++++++++++++++++
.../Preprocessing/TaikoDifficultyHitObject.cs | 1 +
.../Difficulty/TaikoDifficultyCalculator.cs | 6 +-
.../Rhythm => Utils}/IHasInterval.cs | 4 +-
.../Difficulty/Utils/IntervalGroupingUtils.cs | 64 +++++++++++++++++++
8 files changed, 152 insertions(+), 134 deletions(-)
delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs
create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
rename osu.Game.Rulesets.Taiko/Difficulty/{Preprocessing/Rhythm => Utils}/IHasInterval.cs (73%)
create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs
deleted file mode 100644
index cc389d4091..0000000000
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/IntervalGroupedHitObjects.cs
+++ /dev/null
@@ -1,64 +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.Framework.Utils;
-
-namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
-{
- ///
- /// A base class for grouping s by their interval. In edges where an interval change
- /// occurs, the is added to the group with the smaller interval.
- ///
- public abstract class IntervalGroupedHitObjects
- where TChildType : IHasInterval
- {
- public IReadOnlyList Children { get; private set; }
-
- ///
- /// Create a new from a list of s, and add
- /// them to the list until the end of the group.
- ///
- /// The list of s.
- ///
- /// Index in to start adding children. This will be modified and should be passed into
- /// the next 's constructor.
- ///
- ///
- /// The margin of error for the interval, within of which no interval change is considered to have occured.
- ///
- protected IntervalGroupedHitObjects(List data, ref int i, double marginOfError)
- {
- List children = new List();
- Children = children;
- children.Add(data[i]);
- i++;
-
- for (; i < data.Count - 1; i++)
- {
- // An interval change occured, add the current data if the next interval is larger.
- if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError))
- {
- if (data[i + 1].Interval > data[i].Interval + marginOfError)
- {
- children.Add(data[i]);
- i++;
- }
-
- return;
- }
-
- // No interval change occured
- children.Add(data[i]);
- }
-
- // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error.
- // If true, add the current object to the group and increment the index to process the next object.
- if (data.Count > 2 && Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError))
- {
- children.Add(data[i]);
- i++;
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs
index d4cbc9c1f9..cb22b2ef82 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs
@@ -9,9 +9,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
///
/// Represents grouped by their 's interval.
///
- public class SamePatternsGroupedHitObjects : IntervalGroupedHitObjects
+ public class SamePatternsGroupedHitObjects
{
- public SamePatternsGroupedHitObjects? Previous { get; private set; }
+ public IReadOnlyList Children { get; }
+
+ public SamePatternsGroupedHitObjects? Previous { get; }
///
/// The between children within this group.
@@ -29,27 +31,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children);
- private SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List data, ref int i)
- : base(data, ref i, 5)
+ public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List children)
{
Previous = previous;
-
- foreach (TaikoDifficultyHitObject hitObject in AllHitObjects)
- {
- hitObject.Rhythm.SamePatternsGroupedHitObjects = this;
- }
- }
-
- public static void GroupPatterns(List data)
- {
- List samePatterns = new List();
-
- // Index does not need to be incremented, as it is handled within the IntervalGroupedHitObjects constructor.
- for (int i = 0; i < data.Count;)
- {
- SamePatternsGroupedHitObjects? previous = samePatterns.Count > 0 ? samePatterns[^1] : null;
- samePatterns.Add(new SamePatternsGroupedHitObjects(previous, data, ref i));
- }
+ Children = children;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs
index 0b59433a2e..dc6cf45d23 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs
@@ -3,14 +3,17 @@
using System.Collections.Generic;
using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Taiko.Difficulty.Utils;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
{
///
/// Represents a group of s with no rhythm variation.
///
- public class SameRhythmGroupedHitObjects : IntervalGroupedHitObjects, IHasInterval
+ public class SameRhythmGroupedHitObjects : IHasInterval
{
+ public List Children { get; private set; }
+
public TaikoDifficultyHitObject FirstHitObject => Children[0];
public SameRhythmGroupedHitObjects? Previous;
@@ -40,53 +43,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
///
public double Interval { get; private set; }
- public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List data, ref int i)
- : base(data, ref i, 5)
+ public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List children)
{
Previous = previous;
+ Children = children;
- foreach (var hitObject in Children)
- {
- hitObject.Rhythm.SameRhythmGroupedHitObjects = this;
+ // Calculate the average interval between hitobjects, or null if there are fewer than two
+ HitObjectInterval = Children.Count < 2 ? null : Duration / (Children.Count - 1);
- // Pass the HitObjectInterval to each child.
- hitObject.HitObjectInterval = HitObjectInterval;
- }
+ // Calculate the ratio between this group's interval and the previous group's interval
+ HitObjectIntervalRatio = Previous?.HitObjectInterval != null && HitObjectInterval != null
+ ? HitObjectInterval.Value / Previous.HitObjectInterval.Value
+ : 1;
- calculateIntervals();
- }
-
- public static List GroupHitObjects(List data)
- {
- List flatPatterns = new List();
-
- // Index does not need to be incremented, as it is handled within IntervalGroupedHitObjects's constructor.
- for (int i = 0; i < data.Count;)
- {
- SameRhythmGroupedHitObjects? previous = flatPatterns.Count > 0 ? flatPatterns[^1] : null;
- flatPatterns.Add(new SameRhythmGroupedHitObjects(previous, data, ref i));
- }
-
- return flatPatterns;
- }
-
- private void calculateIntervals()
- {
- // Calculate the average interval between hitobjects, or null if there are fewer than two.
- HitObjectInterval = Children.Count < 2 ? null : (Children[^1].StartTime - Children[0].StartTime) / (Children.Count - 1);
-
- // If both the current and previous intervals are available, calculate the ratio.
- if (Previous?.HitObjectInterval != null && HitObjectInterval != null)
- {
- HitObjectIntervalRatio = HitObjectInterval.Value / Previous.HitObjectInterval.Value;
- }
-
- if (Previous == null)
- {
- return;
- }
-
- Interval = StartTime - Previous.StartTime;
+ // Calculate the interval from the previous group's start time
+ Interval = Previous != null ? StartTime - Previous.StartTime : 0;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
new file mode 100644
index 0000000000..fa2135caf3
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
@@ -0,0 +1,63 @@
+// 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.Difficulty.Preprocessing.Rhythm.Data;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
+{
+ public static class TaikoRhythmDifficultyPreprocessor
+ {
+ public static void ProcessAndAssign(List hitObjects)
+ {
+ var rhythmGroups = createSameRhythmGroupedHitObjects(hitObjects);
+
+ foreach (var rhythmGroup in rhythmGroups)
+ {
+ foreach (var hitObject in rhythmGroup.Children)
+ {
+ hitObject.Rhythm.SameRhythmGroupedHitObjects = rhythmGroup;
+ hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval;
+ }
+ }
+
+ var patternGroups = createSamePatternGroupedHitObjects(rhythmGroups);
+
+ foreach (var patternGroup in patternGroups)
+ {
+ foreach (var hitObject in patternGroup.AllHitObjects)
+ {
+ hitObject.Rhythm.SamePatternsGroupedHitObjects = patternGroup;
+ }
+ }
+ }
+
+ private static List createSameRhythmGroupedHitObjects(List hitObjects)
+ {
+ var rhythmGroups = new List();
+ var groups = IntervalGroupingUtils.GroupByInterval(hitObjects);
+
+ foreach (var group in groups)
+ {
+ var previous = rhythmGroups.Count > 0 ? rhythmGroups[^1] : null;
+ rhythmGroups.Add(new SameRhythmGroupedHitObjects(previous, group));
+ }
+
+ return rhythmGroups;
+ }
+
+ private static List createSamePatternGroupedHitObjects(List rhythmGroups)
+ {
+ var patternGroups = new List();
+ var groups = IntervalGroupingUtils.GroupByInterval(rhythmGroups);
+
+ foreach (var group in groups)
+ {
+ var previous = patternGroups.Count > 0 ? patternGroups[^1] : null;
+ patternGroups.Add(new SamePatternsGroupedHitObjects(previous, group));
+ }
+
+ return patternGroups;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
index 34c4871a42..0c668797cd 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
@@ -9,6 +9,7 @@ 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;
+using osu.Game.Rulesets.Taiko.Difficulty.Utils;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
{
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index e07a965ab0..acd654f9b8 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -13,7 +13,7 @@ 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.Preprocessing.Rhythm.Data;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Scoring;
@@ -91,9 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
}
TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects);
-
- var groupedHitObjects = SameRhythmGroupedHitObjects.GroupHitObjects(noteObjects);
- SamePatternsGroupedHitObjects.GroupPatterns(groupedHitObjects);
+ TaikoRhythmDifficultyPreprocessor.ProcessAndAssign(noteObjects);
return difficultyHitObjects;
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs
similarity index 73%
rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs
rename to osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs
index 32b148da2e..8f80bb6079 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/IHasInterval.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs
@@ -1,10 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
+namespace osu.Game.Rulesets.Taiko.Difficulty.Utils
{
///
- /// The interface for hitobjects that provide an interval value.
+ /// The interface for objects that provide an interval value.
///
public interface IHasInterval
{
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
new file mode 100644
index 0000000000..22ded8a966
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
@@ -0,0 +1,64 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Taiko.Difficulty.Utils;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
+{
+ public static class IntervalGroupingUtils
+ {
+ public static List> GroupByInterval(IReadOnlyList data, double marginOfError = 5) where T : IHasInterval
+ {
+ var groups = new List>();
+ if (data.Count == 0)
+ return groups;
+
+ int i = 0;
+
+ while (i < data.Count)
+ {
+ var group = createGroup(data, ref i, marginOfError);
+ groups.Add(group);
+ }
+
+ return groups;
+ }
+
+ private static List createGroup(IReadOnlyList data, ref int i, double marginOfError) where T : IHasInterval
+ {
+ var children = new List { data[i] };
+ i++;
+
+ for (; i < data.Count - 1; i++)
+ {
+ // An interval change occured, add the current data if the next interval is larger.
+ if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError))
+ {
+ if (data[i + 1].Interval > data[i].Interval + marginOfError)
+ {
+ children.Add(data[i]);
+ i++;
+ }
+
+ return children;
+ }
+
+ // No interval change occurred
+ children.Add(data[i]);
+ }
+
+ // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error.
+ // If true, add the current object to the group and increment the index to process the next object.
+ if (data.Count > 2 && i < data.Count &&
+ Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError))
+ {
+ children.Add(data[i]);
+ i++;
+ }
+
+ return children;
+ }
+ }
+}
From 02369baec43f0a68a26a960bef20980289b1f6ab Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 22 Jan 2025 21:44:45 +0900
Subject: [PATCH 051/262] Join/Leave rooms via multiplayer server
Relevant functionality has been removed from `RoomManager` in the
process.
---
.../TestSceneMultiplayerLoungeSubScreen.cs | 26 ------
.../Online/Multiplayer/MultiplayerClient.cs | 3 +
osu.Game/Online/Rooms/CreateRoomRequest.cs | 2 +-
osu.Game/Online/Rooms/JoinRoomRequest.cs | 1 +
.../OnlinePlay/Components/RoomManager.cs | 80 -------------------
.../DailyChallenge/DailyChallenge.cs | 10 +--
osu.Game/Screens/OnlinePlay/IRoomManager.cs | 22 -----
.../OnlinePlay/Lounge/LoungeSubScreen.cs | 9 ++-
.../Screens/OnlinePlay/Match/RoomSubScreen.cs | 6 +-
.../OnlinePlay/Multiplayer/Multiplayer.cs | 3 -
.../Multiplayer/MultiplayerLoungeSubScreen.cs | 34 ++++----
.../Multiplayer/MultiplayerMatchSubScreen.cs | 2 +
.../Multiplayer/MultiplayerRoomManager.cs | 72 -----------------
.../Screens/OnlinePlay/OnlinePlayScreen.cs | 12 +--
.../Screens/OnlinePlay/OnlinePlaySubScreen.cs | 4 -
.../Playlists/PlaylistsLoungeSubScreen.cs | 15 ++++
.../Playlists/PlaylistsRoomSettingsOverlay.cs | 9 ++-
.../Playlists/PlaylistsRoomSubScreen.cs | 2 +
.../Multiplayer/MultiplayerTestScene.cs | 2 +-
.../Multiplayer/TestMultiplayerRoomManager.cs | 10 +--
.../Visual/OnlinePlay/TestRoomManager.cs | 13 ++-
21 files changed, 74 insertions(+), 263 deletions(-)
delete mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
index 9951f62c77..d06a91433d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Visual.OnlinePlay;
@@ -21,23 +20,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
private LoungeSubScreen loungeScreen = null!;
- private Room? lastJoinedRoom;
- private string? lastJoinedPassword;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("push screen", () => LoadScreen(loungeScreen = new MultiplayerLoungeSubScreen()));
-
AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen());
-
- AddStep("bind to event", () =>
- {
- lastJoinedRoom = null;
- lastJoinedPassword = null;
- RoomManager.JoinRoomRequested = onRoomJoined;
- });
}
[Test]
@@ -46,9 +35,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
-
- AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
- AddAssert("room join password correct", () => lastJoinedPassword == null);
}
[Test]
@@ -126,9 +112,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick());
-
- AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
- AddAssert("room join password correct", () => lastJoinedPassword == "password");
}
[Test]
@@ -142,15 +125,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password");
AddStep("press enter", () => InputManager.Key(Key.Enter));
-
- AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
- AddAssert("room join password correct", () => lastJoinedPassword == "password");
- }
-
- private void onRoomJoined(Room room, string? password)
- {
- lastJoinedRoom = room;
- lastJoinedPassword = password;
}
}
}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index e5eade8c1d..7dfe974651 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -253,6 +253,9 @@ namespace osu.Game.Online.Multiplayer
public Task LeaveRoom()
{
+ if (Room == null)
+ return Task.CompletedTask;
+
// The join may have not completed yet, so certain tasks that either update the room or reference the room should be cancelled.
// This includes the setting of Room itself along with the initial update of the room settings on join.
joinCancellationSource?.Cancel();
diff --git a/osu.Game/Online/Rooms/CreateRoomRequest.cs b/osu.Game/Online/Rooms/CreateRoomRequest.cs
index 63a3b7bfa8..9773bb5e7d 100644
--- a/osu.Game/Online/Rooms/CreateRoomRequest.cs
+++ b/osu.Game/Online/Rooms/CreateRoomRequest.cs
@@ -15,6 +15,7 @@ namespace osu.Game.Online.Rooms
public CreateRoomRequest(Room room)
{
Room = room;
+ Success += r => Room.CopyFrom(r);
}
protected override WebRequest CreateWebRequest()
@@ -23,7 +24,6 @@ namespace osu.Game.Online.Rooms
req.ContentType = "application/json";
req.Method = HttpMethod.Post;
-
req.AddRaw(JsonConvert.SerializeObject(Room));
return req;
diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs
index dfc7a53fb2..13e7ac8c84 100644
--- a/osu.Game/Online/Rooms/JoinRoomRequest.cs
+++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs
@@ -16,6 +16,7 @@ namespace osu.Game.Online.Rooms
{
Room = room;
Password = password;
+ Success += r => Room.CopyFrom(r);
}
protected override WebRequest CreateWebRequest()
diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
index 73f980f0a3..3abb4098fb 100644
--- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
@@ -5,12 +5,10 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Logging;
-using osu.Game.Online.API;
using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Components
@@ -23,89 +21,11 @@ namespace osu.Game.Screens.OnlinePlay.Components
public IBindableList Rooms => rooms;
- protected IBindable JoinedRoom => joinedRoom;
- private readonly Bindable joinedRoom = new Bindable();
-
- [Resolved]
- private IAPIProvider api { get; set; } = null!;
-
public RoomManager()
{
RelativeSizeAxes = Axes.Both;
}
- protected override void Dispose(bool isDisposing)
- {
- base.Dispose(isDisposing);
- PartRoom();
- }
-
- public virtual void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null)
- {
- room.Host = api.LocalUser.Value;
-
- var req = new CreateRoomRequest(room);
-
- req.Success += result =>
- {
- joinedRoom.Value = room;
-
- AddOrUpdateRoom(result);
- room.CopyFrom(result); // Also copy back to the source model, since this is likely to have been stored elsewhere.
-
- // The server may not contain all properties (such as password), so invoke success with the given room.
- onSuccess?.Invoke(room);
- };
-
- req.Failure += exception =>
- {
- onError?.Invoke(req.Response?.Error ?? exception.Message);
- };
-
- api.Queue(req);
- }
-
- private JoinRoomRequest? currentJoinRoomRequest;
-
- public virtual void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null)
- {
- currentJoinRoomRequest?.Cancel();
- currentJoinRoomRequest = new JoinRoomRequest(room, password);
-
- currentJoinRoomRequest.Success += result =>
- {
- joinedRoom.Value = room;
-
- AddOrUpdateRoom(result);
- room.CopyFrom(result); // Also copy back to the source model, since this is likely to have been stored elsewhere.
-
- onSuccess?.Invoke(room);
- };
-
- currentJoinRoomRequest.Failure += exception =>
- {
- if (exception is OperationCanceledException)
- return;
-
- onError?.Invoke(exception.Message);
- };
-
- api.Queue(currentJoinRoomRequest);
- }
-
- public virtual void PartRoom()
- {
- currentJoinRoomRequest?.Cancel();
-
- if (joinedRoom.Value == null)
- return;
-
- if (api.State.Value == APIState.Online)
- api.Queue(new PartRoomRequest(joinedRoom.Value));
-
- joinedRoom.Value = null;
- }
-
private readonly HashSet ignoredRooms = new HashSet();
public void AddOrUpdateRoom(Room room)
diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs
index 13a282dd52..e3d6d42c05 100644
--- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs
+++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs
@@ -34,7 +34,6 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
-using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Match.Components;
@@ -71,9 +70,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
- [Cached(Type = typeof(IRoomManager))]
- private RoomManager roomManager { get; set; }
-
[Cached]
private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
@@ -115,7 +111,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
this.room = room;
playlistItem = room.Playlist.Single();
- roomManager = new RoomManager();
Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING };
}
@@ -131,7 +126,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- roomManager,
beatmapAvailabilityTracker,
new ScreenStack(new RoomBackgroundScreen(playlistItem))
{
@@ -426,7 +420,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
base.OnEntering(e);
waves.Show();
- roomManager.JoinRoom(room);
+ API.Queue(new JoinRoomRequest(room, null));
startLoopingTrack(this, musicController);
metadataClient.BeginWatchingMultiplayerRoom(room.RoomID!.Value).ContinueWith(t =>
@@ -480,7 +474,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
previewTrackManager.StopAnyPlaying(this);
this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
- roomManager.PartRoom();
+ API.Queue(new PartRoomRequest(room));
metadataClient.EndWatchingMultiplayerRoom(room.RoomID!.Value).FireAndForget();
return base.OnExiting(e);
diff --git a/osu.Game/Screens/OnlinePlay/IRoomManager.cs b/osu.Game/Screens/OnlinePlay/IRoomManager.cs
index ed4fb7b15e..8ecb1dd7e0 100644
--- a/osu.Game/Screens/OnlinePlay/IRoomManager.cs
+++ b/osu.Game/Screens/OnlinePlay/IRoomManager.cs
@@ -38,27 +38,5 @@ namespace osu.Game.Screens.OnlinePlay
/// Removes all s from this .
///
void ClearRooms();
-
- ///
- /// Creates a new .
- ///
- /// The to create.
- /// An action to be invoked if the creation succeeds.
- /// An action to be invoked if an error occurred.
- void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null);
-
- ///
- /// Joins a .
- ///
- /// The to join. must be populated.
- /// An optional password to use for the join operation.
- ///
- ///
- void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null);
-
- ///
- /// Parts the currently-joined .
- ///
- void PartRoom();
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
index f00cf7427c..f3f4df166a 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
@@ -263,6 +263,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
music.EnsurePlayingSomething();
onReturning();
+
+ // Poll for any newly-created rooms (including potentially the user's own).
+ ListingPollingComponent.PollImmediately();
}
public override bool OnExiting(ScreenExitEvent e)
@@ -297,14 +300,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
popoverContainer.HidePopover();
}
- public virtual void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null) => Schedule(() =>
+ public void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null) => Schedule(() =>
{
if (joiningRoomOperation != null)
return;
joiningRoomOperation = ongoingOperationTracker?.BeginOperation();
- RoomManager?.JoinRoom(room, password, _ =>
+ TryJoin(room, password, r =>
{
Open(room);
joiningRoomOperation?.Dispose();
@@ -318,6 +321,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
});
});
+ protected abstract void TryJoin(Room room, string? password, Action onSuccess, Action onFailure);
+
///
/// Copies a room and opens it as a fresh (not-yet-created) one.
///
diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
index 4ef31c02c3..d37f3b877c 100644
--- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
@@ -343,7 +343,9 @@ namespace osu.Game.Screens.OnlinePlay.Match
if (!ensureExitConfirmed())
return true;
- RoomManager?.PartRoom();
+ if (Room.RoomID != null)
+ PartRoom();
+
Mods.Value = Array.Empty();
onLeaving();
@@ -351,6 +353,8 @@ namespace osu.Game.Screens.OnlinePlay.Match
return base.OnExiting(e);
}
+ protected abstract void PartRoom();
+
private bool ensureExitConfirmed()
{
if (ExitConfirmed)
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
index bf316bb3da..dfed32aebc 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs
@@ -8,7 +8,6 @@ using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
-using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
@@ -97,8 +96,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override string ScreenTitle => "Multiplayer";
- protected override RoomManager CreateRoomManager() => new MultiplayerRoomManager();
-
protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen();
public void Join(Room room, string? password) => Schedule(() => Lounge.Join(room, password));
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
index dd61caa3db..e901ecbdce 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.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 System;
using System.Collections.Generic;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Logging;
-using osu.Framework.Screens;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Configuration;
@@ -32,19 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private Dropdown roomAccessTypeDropdown = null!;
private OsuCheckbox showInProgress = null!;
- public override void OnResuming(ScreenTransitionEvent e)
- {
- base.OnResuming(e);
-
- // Upon having left a room, we don't know whether we were the only participant, and whether the room is now closed as a result of leaving it.
- // To work around this, temporarily remove the room and trigger an immediate listing poll.
- if (e.Last is MultiplayerMatchSubScreen match)
- {
- RoomManager?.RemoveRoom(match.Room);
- ListingPollingComponent.PollImmediately();
- }
- }
-
protected override IEnumerable CreateFilterControls()
{
foreach (var control in base.CreateFilterControls())
@@ -93,6 +81,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent();
+ protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure)
+ {
+ client.JoinRoom(room, password).ContinueWith(result =>
+ {
+ if (result.IsCompletedSuccessfully)
+ onSuccess(room);
+ else
+ {
+ const string message = "Failed to join multiplayer room.";
+
+ if (result.Exception != null)
+ Logger.Error(result.Exception, message);
+
+ onFailure.Invoke(result.Exception?.AsSingular().Message ?? message);
+ }
+ });
+ }
+
protected override void OpenNewRoom(Room room)
{
if (!client.IsConnected.Value)
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index 06ea5ee033..553c0c9182 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -278,6 +278,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return base.OnExiting(e);
}
+ protected override void PartRoom() => client.LeaveRoom();
+
private ModSettingChangeTracker? modSettingChangeTracker;
private ScheduledDelegate? debouncedModSettingsUpdate;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs
deleted file mode 100644
index 7f09c9cbe9..0000000000
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Diagnostics;
-using osu.Framework.Allocation;
-using osu.Framework.Extensions.ExceptionExtensions;
-using osu.Framework.Logging;
-using osu.Game.Online.Multiplayer;
-using osu.Game.Online.Rooms;
-using osu.Game.Screens.OnlinePlay.Components;
-
-namespace osu.Game.Screens.OnlinePlay.Multiplayer
-{
- public partial class MultiplayerRoomManager : RoomManager
- {
- [Resolved]
- private MultiplayerClient multiplayerClient { get; set; } = null!;
-
- public override void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null)
- => base.CreateRoom(room, r => joinMultiplayerRoom(r, r.Password, onSuccess, onError), onError);
-
- public override void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null)
- {
- if (!multiplayerClient.IsConnected.Value)
- {
- onError?.Invoke("Not currently connected to the multiplayer server.");
- return;
- }
-
- // this is done here as a pre-check to avoid clicking on already closed rooms in the lounge from triggering a server join.
- // should probably be done at a higher level, but due to the current structure of things this is the easiest place for now.
- if (room.HasEnded)
- {
- onError?.Invoke("Cannot join an ended room.");
- return;
- }
-
- base.JoinRoom(room, password, r => joinMultiplayerRoom(r, password, onSuccess, onError), onError);
- }
-
- public override void PartRoom()
- {
- if (JoinedRoom.Value == null)
- return;
-
- base.PartRoom();
- multiplayerClient.LeaveRoom();
- }
-
- private void joinMultiplayerRoom(Room room, string? password, Action? onSuccess = null, Action? onError = null)
- {
- Debug.Assert(room.RoomID != null);
-
- multiplayerClient.JoinRoom(room, password).ContinueWith(t =>
- {
- if (t.IsCompletedSuccessfully)
- Schedule(() => onSuccess?.Invoke(room));
- else if (t.IsFaulted)
- {
- const string message = "Failed to join multiplayer room.";
-
- if (t.Exception != null)
- Logger.Error(t.Exception, message);
-
- PartRoom();
- Schedule(() => onError?.Invoke(t.Exception?.AsSingular().Message ?? message));
- }
- });
- }
- }
-}
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
index 17fb667e14..16462b90c1 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
@@ -36,12 +36,12 @@ namespace osu.Game.Screens.OnlinePlay
private readonly ScreenStack screenStack = new OnlinePlaySubScreenStack { RelativeSizeAxes = Axes.Both };
private OnlinePlayScreenWaveContainer waves = null!;
- [Cached(Type = typeof(IRoomManager))]
- protected RoomManager RoomManager { get; private set; }
-
[Cached]
private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker();
+ [Cached(Type = typeof(IRoomManager))]
+ private readonly RoomManager roomManager = new RoomManager();
+
[Resolved]
protected IAPIProvider API { get; private set; } = null!;
@@ -51,8 +51,6 @@ namespace osu.Game.Screens.OnlinePlay
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING };
-
- RoomManager = CreateRoomManager();
}
private readonly IBindable apiState = new Bindable();
@@ -67,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay
{
screenStack,
new Header(ScreenTitle, screenStack),
- RoomManager,
+ roomManager,
ongoingOperationTracker,
}
};
@@ -165,8 +163,6 @@ namespace osu.Game.Screens.OnlinePlay
subScreen.Exit();
}
- RoomManager.PartRoom();
-
waves.Hide();
this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs
index fa1ee004c9..9b35a794a3 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.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.
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Screens;
@@ -15,9 +14,6 @@ namespace osu.Game.Screens.OnlinePlay
protected sealed override bool PlayExitSound => false;
- [Resolved]
- protected IRoomManager? RoomManager { get; private set; }
-
protected OnlinePlaySubScreen()
{
Anchor = Anchor.Centre;
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
index d66b4f844c..92415e0eb1 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
@@ -59,6 +60,20 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return criteria;
}
+ protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure)
+ {
+ var joinRoomRequest = new JoinRoomRequest(room, password);
+
+ joinRoomRequest.Success += r => onSuccess(r);
+ joinRoomRequest.Failure += exception =>
+ {
+ if (exception is not OperationCanceledException)
+ onFailure(exception.Message);
+ };
+
+ api.Queue(joinRoomRequest);
+ }
+
protected override OsuButton CreateNewRoomButton() => new CreatePlaylistsRoomButton();
protected override Room CreateNewRoom()
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
index 88af161cc8..b3d1d577ed 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
@@ -75,9 +75,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private PurpleRoundedButton editPlaylistButton = null!;
- [Resolved]
- private IRoomManager? manager { get; set; }
-
[Resolved]
private IAPIProvider api { get; set; } = null!;
@@ -449,7 +446,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
room.Duration = DurationField.Current.Value;
loadingLayer.Show();
- manager?.CreateRoom(room, onSuccess, onError);
+
+ var req = new CreateRoomRequest(room);
+ req.Success += onSuccess;
+ req.Failure += e => onError(req.Response?.Error ?? e.Message);
+ api.Queue(req);
}
private void hideError() => ErrorText.FadeOut(50);
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs
index 9b4630ac0b..064c355a69 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs
@@ -290,6 +290,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}));
}
+ protected override void PartRoom() => api.Queue(new PartRoomRequest(Room));
+
protected override Screen CreateGameplayScreen(PlaylistItem selectedItem)
{
return new PlayerLoader(() => new PlaylistsPlayer(Room, selectedItem)
diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
index 42cf317829..dca1fc8f3c 100644
--- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("join room", () =>
{
SelectedRoom.Value = CreateRoom();
- RoomManager.CreateRoom(SelectedRoom.Value);
+ API.Queue(new CreateRoomRequest(SelectedRoom.Value));
});
AddUntilStep("wait for room join", () => RoomJoined);
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
index b998a638e5..59ac9a9749 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
@@ -1,12 +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 System.Collections.Generic;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
-using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer
@@ -15,7 +13,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// A for use in multiplayer test scenes.
/// Should generally not be used by itself outside of a .
///
- public partial class TestMultiplayerRoomManager : MultiplayerRoomManager
+ public partial class TestMultiplayerRoomManager : RoomManager
{
private readonly TestRoomRequestsHandler requestsHandler;
@@ -26,12 +24,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
public IReadOnlyList ServerSideRooms => requestsHandler.ServerSideRooms;
- public override void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null)
- => base.CreateRoom(room, r => onSuccess?.Invoke(r), onError);
-
- public override void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null)
- => base.JoinRoom(room, password, r => onSuccess?.Invoke(r), onError);
-
///
/// Adds a room to a local "server-side" list that's returned when a is fired.
///
diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs
index b1e3eafacc..60d169a46f 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs
@@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Allocation;
using osu.Game.Beatmaps;
+using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@@ -15,15 +17,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay
///
public partial class TestRoomManager : RoomManager
{
- public Action? JoinRoomRequested;
-
private int currentRoomId;
- public override void JoinRoom(Room room, string? password = null, Action? onSuccess = null, Action? onError = null)
- {
- JoinRoomRequested?.Invoke(room, password);
- base.JoinRoom(room, password, onSuccess, onError);
- }
+ [Resolved]
+ private IAPIProvider api { get; set; } = null!;
public void AddRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false)
{
@@ -49,7 +46,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
public void AddRoom(Room room)
{
room.RoomID = -currentRoomId;
- CreateRoom(room);
+ api.Queue(new CreateRoomRequest(room));
currentRoomId++;
}
}
From 2c0d6b14c82969a850b292f785a678016e06ed26 Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Wed, 22 Jan 2025 13:24:30 +0000
Subject: [PATCH 052/262] Fix incorrect namespace
---
.../Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs | 1 +
.../Difficulty/Utils/IntervalGroupingUtils.cs | 3 +--
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
index fa2135caf3..cd56d835dc 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
+using osu.Game.Rulesets.Taiko.Difficulty.Utils;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
{
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
index 22ded8a966..3b6f5406b4 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
@@ -3,9 +3,8 @@
using System.Collections.Generic;
using osu.Framework.Utils;
-using osu.Game.Rulesets.Taiko.Difficulty.Utils;
-namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
+namespace osu.Game.Rulesets.Taiko.Difficulty.Utils
{
public static class IntervalGroupingUtils
{
From 753e9ef7c79f85d027557295c0c60fb4fa09210c Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Wed, 22 Jan 2025 13:26:12 +0000
Subject: [PATCH 053/262] Keep old behaviour of `double.PositiveInfinity` being
the default for `Interval`
---
.../Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs
index dc6cf45d23..4f7023059f 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
: 1;
// Calculate the interval from the previous group's start time
- Interval = Previous != null ? StartTime - Previous.StartTime : 0;
+ Interval = Previous != null ? StartTime - Previous.StartTime : double.PositiveInfinity;
}
}
}
From 9a623257f5bd8cfed7f2d691fbb1c2959483c111 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Thu, 23 Jan 2025 16:19:09 +0900
Subject: [PATCH 054/262] Adjust + fix tests
---
.../StatefulMultiplayerClientTest.cs | 7 +-
.../TestSceneDrawableLoungeRoom.cs | 2 +-
.../Multiplayer/TestSceneMultiplayer.cs | 15 ++--
.../TestSceneMultiplayerLoungeSubScreen.cs | 58 +++++++++++---
.../TestSceneMultiplayerPlaylist.cs | 10 +--
.../TestScenePlaylistsLoungeSubScreen.cs | 30 ++++++-
.../TestScenePlaylistsMatchSettingsOverlay.cs | 78 +++++++------------
.../Visual/TestMultiplayerComponents.cs | 24 ++----
osu.Game/Online/Rooms/Room.cs | 17 ++++
.../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 14 +---
.../OnlinePlay/Lounge/IOnlinePlayLounge.cs | 32 ++++++++
.../OnlinePlay/Lounge/LoungeSubScreen.cs | 19 +++--
.../Screens/OnlinePlay/OnlinePlayScreen.cs | 2 -
.../IMultiplayerTestSceneDependencies.cs | 6 --
.../Multiplayer/MultiplayerTestScene.cs | 3 +-
.../MultiplayerTestSceneDependencies.cs | 6 +-
.../Multiplayer/TestMultiplayerClient.cs | 35 +++++++--
.../Multiplayer/TestMultiplayerRoomManager.cs | 34 --------
.../OnlinePlayTestSceneDependencies.cs | 4 +-
.../Visual/OnlinePlay/TestRoomManager.cs | 20 +++--
20 files changed, 232 insertions(+), 184 deletions(-)
create mode 100644 osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs
delete mode 100644 osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
index 559db16751..be30e06ed4 100644
--- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
+++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
@@ -8,7 +8,6 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
-using osu.Game.Online.Rooms;
using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.NonVisual.Multiplayer
@@ -72,10 +71,6 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddStep("create room initially in gameplay", () =>
{
- var newRoom = new Room();
- newRoom.CopyFrom(SelectedRoom.Value!);
-
- newRoom.RoomID = null;
MultiplayerClient.RoomSetupAction = room =>
{
room.State = MultiplayerRoomState.Playing;
@@ -86,7 +81,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
});
};
- RoomManager.CreateRoom(newRoom);
+ MultiplayerClient.JoinRoom(MultiplayerClient.ServerSideRooms.Single()).ConfigureAwait(false);
});
AddUntilStep("wait for room join", () => RoomJoined);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs
index c5fb52461a..459a90d096 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load()
{
- var mockLounge = new Mock();
+ var mockLounge = new Mock();
mockLounge
.Setup(l => l.Join(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>()))
.Callback, Action>((_, _, _, d) =>
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index fb653cea8b..0966c61a3a 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -58,7 +58,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private TestMultiplayerComponents multiplayerComponents = null!;
private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient;
- private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
@@ -257,7 +256,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room", () =>
{
- roomManager.AddServerSideRoom(new Room
+ multiplayerClient.AddServerSideRoom(new Room
{
Name = "Test Room",
Playlist =
@@ -286,7 +285,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room", () =>
{
- roomManager.AddServerSideRoom(new Room
+ multiplayerClient.AddServerSideRoom(new Room
{
Name = "Test Room",
Playlist =
@@ -336,7 +335,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room", () =>
{
- roomManager.AddServerSideRoom(new Room
+ multiplayerClient.AddServerSideRoom(new Room
{
Name = "Test Room",
Password = "password",
@@ -789,7 +788,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room", () =>
{
- roomManager.AddServerSideRoom(new Room
+ multiplayerClient.AddServerSideRoom(new Room
{
Name = "Test Room",
QueueMode = QueueMode.AllPlayers,
@@ -810,8 +809,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0);
AddStep("change server-side settings", () =>
{
- roomManager.ServerSideRooms[0].Name = "New name";
- roomManager.ServerSideRooms[0].Playlist =
+ multiplayerClient.ServerSideRooms[0].Name = "New name";
+ multiplayerClient.ServerSideRooms[0].Playlist =
[
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
@@ -828,7 +827,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("local room has correct settings", () =>
{
var localRoom = this.ChildrenOfType().Single().Room;
- return localRoom.Name == roomManager.ServerSideRooms[0].Name && localRoom.Playlist.Single().ID == 2;
+ return localRoom.Name == multiplayerClient.ServerSideRooms[0].Name && localRoom.Playlist.Single().ID == 2;
});
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
index d06a91433d..4a259149e2 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
@@ -9,18 +9,26 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.OnlinePlay.Lounge;
+using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public partial class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene
+ public partial class TestSceneMultiplayerLoungeSubScreen : MultiplayerTestScene
{
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
private LoungeSubScreen loungeScreen = null!;
+ private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First();
+
+ public TestSceneMultiplayerLoungeSubScreen()
+ : base(false)
+ {
+ }
+
public override void SetUpSteps()
{
base.SetUpSteps();
@@ -32,15 +40,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestJoinRoomWithoutPassword()
{
- AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false));
+ addRoom(false);
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
+
+ AddAssert("room joined", () => MultiplayerClient.RoomJoined);
}
[Test]
public void TestPopoverHidesOnBackButton()
{
- AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ addRoom(true);
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
@@ -53,18 +63,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("hit escape", () => InputManager.Key(Key.Escape));
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any());
+
+ AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
}
[Test]
public void TestPopoverHidesOnLeavingScreen()
{
- AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ addRoom(true);
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any());
AddStep("exit screen", () => Stack.Exit());
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any());
+
+ AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
}
[Test]
@@ -72,16 +86,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
- AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ addRoom(true);
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "wrong");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick());
- AddAssert("room not joined", () => loungeScreen.IsCurrentScreen());
+ AddAssert("still at lounge", () => loungeScreen.IsCurrentScreen());
AddUntilStep("password prompt still visible", () => passwordEntryPopover!.State.Value == Visibility.Visible);
AddAssert("textbox still focused", () => InputManager.FocusedDrawable is OsuPasswordTextBox);
+
+ AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
}
[Test]
@@ -89,16 +105,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
- AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ addRoom(true);
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "wrong");
AddStep("press enter", () => InputManager.Key(Key.Enter));
- AddAssert("room not joined", () => loungeScreen.IsCurrentScreen());
+ AddAssert("still at lounge", () => loungeScreen.IsCurrentScreen());
AddUntilStep("password prompt still visible", () => passwordEntryPopover!.State.Value == Visibility.Visible);
AddAssert("textbox still focused", () => InputManager.FocusedDrawable is OsuPasswordTextBox);
+
+ AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
}
[Test]
@@ -106,12 +124,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
- AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ addRoom(true);
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick());
+
+ AddUntilStep("room joined", () => MultiplayerClient.RoomJoined);
}
[Test]
@@ -119,12 +139,30 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
- AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ addRoom(true);
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password");
AddStep("press enter", () => InputManager.Key(Key.Enter));
+
+ AddAssert("room joined", () => MultiplayerClient.RoomJoined);
}
+
+ private void addRoom(bool withPassword)
+ {
+ int initialRoomCount = 0;
+
+ AddStep("add room", () =>
+ {
+ initialRoomCount = roomsContainer.Rooms.Count;
+ RoomManager.AddRooms(1, withPassword: withPassword);
+ loungeScreen.RefreshRooms();
+ });
+
+ AddUntilStep("wait for room to appear", () => roomsContainer.Rooms.Count == initialRoomCount + 1);
+ }
+
+ protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies();
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
index 36f5bba384..77b75f407b 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
@@ -127,7 +127,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
addItemStep();
AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely());
- AddStep("leave room", () => RoomManager.PartRoom());
+ AddStep("leave room", () => MultiplayerClient.LeaveRoom());
AddUntilStep("wait for room part", () => !RoomJoined);
AddUntilStep("item 0 not in lists", () => !inHistoryList(0) && !inQueueList(0));
@@ -148,7 +148,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely());
assertQueueTabCount(2);
- AddStep("leave room", () => RoomManager.PartRoom());
+ AddStep("leave room", () => MultiplayerClient.LeaveRoom());
AddUntilStep("wait for room part", () => !RoomJoined);
assertQueueTabCount(0);
}
@@ -157,12 +157,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestJoinRoomWithMixedItemsAddedInCorrectLists()
{
- AddStep("leave room", () => RoomManager.PartRoom());
+ AddStep("leave room", () => MultiplayerClient.LeaveRoom());
AddUntilStep("wait for room part", () => !RoomJoined);
AddStep("join room with items", () =>
{
- RoomManager.CreateRoom(new Room
+ API.Queue(new CreateRoomRequest(new Room
{
Name = "test name",
Playlist =
@@ -177,7 +177,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Expired = true
}
]
- });
+ }));
});
AddUntilStep("wait for room join", () => RoomJoined);
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
index 8c8dc8d69a..0897a3b2f5 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
@@ -35,7 +35,13 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestManyRooms()
{
- AddStep("add rooms", () => RoomManager.AddRooms(500));
+ AddStep("add rooms", () =>
+ {
+ RoomManager.AddRooms(500);
+ loungeScreen.RefreshRooms();
+ });
+
+ AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 500);
}
[Test]
@@ -43,7 +49,12 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left));
- AddStep("add rooms", () => RoomManager.AddRooms(30));
+ AddStep("add rooms", () =>
+ {
+ RoomManager.AddRooms(30);
+ loungeScreen.RefreshRooms();
+ });
+
AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30);
AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0]));
@@ -60,7 +71,12 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestScrollSelectedIntoView()
{
- AddStep("add rooms", () => RoomManager.AddRooms(30));
+ AddStep("add rooms", () =>
+ {
+ RoomManager.AddRooms(30);
+ loungeScreen.RefreshRooms();
+ });
+
AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30);
AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0]));
@@ -74,7 +90,13 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestEnteringRoomTakesLeaseOnSelection()
{
- AddStep("add rooms", () => RoomManager.AddRooms(1));
+ AddStep("add rooms", () =>
+ {
+ RoomManager.AddRooms(1);
+ loungeScreen.RefreshRooms();
+ });
+
+ AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 1);
AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled);
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
index 5868331451..51e39e1b7f 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
@@ -3,14 +3,13 @@
using System;
using NUnit.Framework;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Online.API;
using osu.Game.Online.Rooms;
-using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Visual.OnlinePlay;
@@ -21,13 +20,33 @@ namespace osu.Game.Tests.Visual.Playlists
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
private TestRoomSettings settings = null!;
-
- protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
+ private Func? handleRequest;
public override void SetUpSteps()
{
base.SetUpSteps();
+ AddStep("setup api", () =>
+ {
+ handleRequest = null;
+ ((DummyAPIAccess)API).HandleRequest = req =>
+ {
+ if (req is not CreateRoomRequest createReq || handleRequest == null)
+ return false;
+
+ if (handleRequest(createReq.Room) is string errorText)
+ createReq.TriggerFailure(new APIException(errorText, null));
+ else
+ {
+ var createdRoom = new APICreatedRoom();
+ createdRoom.CopyFrom(createReq.Room);
+ createReq.TriggerSuccess(createdRoom);
+ }
+
+ return true;
+ };
+ });
+
AddStep("create overlay", () =>
{
SelectedRoom.Value = new Room();
@@ -75,10 +94,10 @@ namespace osu.Game.Tests.Visual.Playlists
settings.DurationField.Current.Value = expectedDuration;
SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)];
- RoomManager.CreateRequested = r =>
+ handleRequest = r =>
{
createdRoom = r;
- return string.Empty;
+ return null;
};
});
@@ -103,7 +122,7 @@ namespace osu.Game.Tests.Visual.Playlists
errorMessage = $"{not_found_prefix} {beatmap.OnlineID}";
- RoomManager.CreateRequested = _ => errorMessage;
+ handleRequest = _ => errorMessage;
});
AddAssert("error not displayed", () => !settings.ErrorText.IsPresent);
@@ -128,7 +147,7 @@ namespace osu.Game.Tests.Visual.Playlists
SelectedRoom.Value!.Name = "Test Room";
SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)];
- RoomManager.CreateRequested = _ => failText;
+ handleRequest = _ => failText;
});
AddAssert("error not displayed", () => !settings.ErrorText.IsPresent);
@@ -159,48 +178,5 @@ namespace osu.Game.Tests.Visual.Playlists
{
}
}
-
- private class TestDependencies : OnlinePlayTestSceneDependencies
- {
- protected override IRoomManager CreateRoomManager() => new TestRoomManager();
- }
-
- protected class TestRoomManager : IRoomManager
- {
- public Func? CreateRequested;
-
- public event Action RoomsUpdated
- {
- add { }
- remove { }
- }
-
- public IBindable InitialRoomsReceived { get; } = new Bindable(true);
-
- public IBindableList Rooms => null!;
-
- public void AddOrUpdateRoom(Room room) => throw new NotImplementedException();
-
- public void RemoveRoom(Room room) => throw new NotImplementedException();
-
- public void ClearRooms() => throw new NotImplementedException();
-
- public void CreateRoom(Room room, Action? onSuccess = null, Action? onError = null)
- {
- if (CreateRequested == null)
- return;
-
- string error = CreateRequested.Invoke(room);
-
- if (!string.IsNullOrEmpty(error))
- onError?.Invoke(error);
- else
- onSuccess?.Invoke(room);
- }
-
- public void JoinRoom(Room room, string? password, Action? onSuccess = null, Action? onError = null) => throw new NotImplementedException();
-
- public void PartRoom() => throw new NotImplementedException();
- }
}
}
diff --git a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs
index 1814fb70c8..e385ff3a03 100644
--- a/osu.Game.Tests/Visual/TestMultiplayerComponents.cs
+++ b/osu.Game.Tests/Visual/TestMultiplayerComponents.cs
@@ -11,7 +11,6 @@ using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens;
-using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Tests.Visual.OnlinePlay;
@@ -26,15 +25,12 @@ namespace osu.Game.Tests.Visual
/// - Provides a to be resolved as a dependency in the screen,
/// which is typically a part of .
/// - Rebinds the to handle requests via a .
- /// - Provides a for the screen.
///
///
///
public partial class TestMultiplayerComponents : OsuScreen
{
- public Screens.OnlinePlay.Multiplayer.Multiplayer MultiplayerScreen => multiplayerScreen;
-
- public TestMultiplayerRoomManager RoomManager => multiplayerScreen.RoomManager;
+ public Screens.OnlinePlay.Multiplayer.Multiplayer MultiplayerScreen { get; }
public IScreen CurrentScreen => screenStack.CurrentScreen;
@@ -53,17 +49,17 @@ namespace osu.Game.Tests.Visual
private BeatmapManager beatmapManager { get; set; }
private readonly OsuScreenStack screenStack;
- private readonly TestMultiplayer multiplayerScreen;
+ private readonly TestRoomRequestsHandler requestsHandler = new TestRoomRequestsHandler();
public TestMultiplayerComponents()
{
- multiplayerScreen = new TestMultiplayer();
+ MultiplayerScreen = new Screens.OnlinePlay.Multiplayer.Multiplayer();
InternalChildren = new Drawable[]
{
userLookupCache,
beatmapLookupCache,
- MultiplayerClient = new TestMultiplayerClient(RoomManager),
+ MultiplayerClient = new TestMultiplayerClient(requestsHandler),
screenStack = new OsuScreenStack
{
Name = nameof(TestMultiplayerComponents),
@@ -71,13 +67,13 @@ namespace osu.Game.Tests.Visual
}
};
- screenStack.Push(multiplayerScreen);
+ screenStack.Push(MultiplayerScreen);
}
[BackgroundDependencyLoader]
private void load(IAPIProvider api)
{
- ((DummyAPIAccess)api).HandleRequest = request => multiplayerScreen.RequestsHandler.HandleRequest(request, api.LocalUser.Value, beatmapManager);
+ ((DummyAPIAccess)api).HandleRequest = request => requestsHandler.HandleRequest(request, api.LocalUser.Value, beatmapManager);
}
public override bool OnBackButton() => (screenStack.CurrentScreen as OsuScreen)?.OnBackButton() ?? base.OnBackButton();
@@ -90,13 +86,5 @@ namespace osu.Game.Tests.Visual
screenStack.Exit();
return true;
}
-
- private partial class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
- {
- public new TestMultiplayerRoomManager RoomManager { get; private set; }
- public TestRoomRequestsHandler RequestsHandler { get; private set; }
-
- protected override RoomManager CreateRoomManager() => RoomManager = new TestMultiplayerRoomManager(RequestsHandler = new TestRoomRequestsHandler());
- }
}
}
diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs
index f8660a656e..c5e292a19d 100644
--- a/osu.Game/Online/Rooms/Room.cs
+++ b/osu.Game/Online/Rooms/Room.cs
@@ -342,6 +342,23 @@ namespace osu.Game.Online.Rooms
// Not yet serialised (not implemented).
private RoomAvailability availability;
+ public Room()
+ {
+ }
+
+ public Room(MultiplayerRoom room)
+ {
+ RoomID = room.RoomID;
+ Name = room.Settings.Name;
+ Password = room.Settings.Password;
+ Type = room.Settings.MatchType;
+ QueueMode = room.Settings.QueueMode;
+ AutoStartDuration = room.Settings.AutoStartDuration;
+ AutoSkip = room.Settings.AutoSkip;
+ Host = room.Host != null ? new APIUser { Id = room.Host.UserID } : null;
+ Playlist = room.Playlist.Select(p => new PlaylistItem(p)).ToArray();
+ }
+
///
/// Copies values from another into this one.
///
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs
index 0a55472c2d..032a231ad3 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs
@@ -24,7 +24,6 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings;
using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Components;
@@ -51,7 +50,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
}
[Resolved(canBeNull: true)]
- private LoungeSubScreen? lounge { get; set; }
+ private IOnlinePlayLounge? lounge { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
@@ -163,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
{
- lounge?.OpenCopy(Room);
+ lounge?.Clone(Room);
})
};
@@ -171,12 +170,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{
items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () =>
{
- dialogOverlay?.Push(new ClosePlaylistDialog(Room, () =>
- {
- var request = new ClosePlaylistRequest(Room.RoomID!.Value);
- request.Success += () => lounge?.RefreshRooms();
- api.Queue(request);
- }));
+ dialogOverlay?.Push(new ClosePlaylistDialog(Room, () => lounge?.Close(Room)));
}));
}
@@ -239,7 +233,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private readonly Room room;
[Resolved(canBeNull: true)]
- private LoungeSubScreen? lounge { get; set; }
+ private IOnlinePlayLounge? lounge { get; set; }
public override bool HandleNonPositionalInput => true;
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs
new file mode 100644
index 0000000000..8fa7d0751f
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs
@@ -0,0 +1,32 @@
+// 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.Online.Rooms;
+
+namespace osu.Game.Screens.OnlinePlay.Lounge
+{
+ public interface IOnlinePlayLounge
+ {
+ ///
+ /// Attempts to join the given room.
+ ///
+ /// The room to join.
+ /// The password.
+ /// A delegate to invoke if the user joined the room.
+ /// A delegate to invoke if the user is not able join the room.
+ void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null);
+
+ ///
+ /// Clones the given room and opens it as a fresh (not-yet-created) one.
+ ///
+ /// The room to clone.
+ void Clone(Room room);
+
+ ///
+ /// Closes the given room.
+ ///
+ /// The room to close.
+ void Close(Room room);
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
index f3f4df166a..df17063fdf 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
@@ -21,6 +21,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets;
@@ -33,7 +34,8 @@ using osuTK;
namespace osu.Game.Screens.OnlinePlay.Lounge
{
[Cached]
- public abstract partial class LoungeSubScreen : OnlinePlaySubScreen
+ [Cached(typeof(IOnlinePlayLounge))]
+ public abstract partial class LoungeSubScreen : OnlinePlaySubScreen, IOnlinePlayLounge
{
public override string Title => "Lounge";
@@ -323,11 +325,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
protected abstract void TryJoin(Room room, string? password, Action onSuccess, Action onFailure);
- ///
- /// Copies a room and opens it as a fresh (not-yet-created) one.
- ///
- /// The room to copy.
- public void OpenCopy(Room room)
+ public void Clone(Room room)
{
Debug.Assert(room.RoomID != null);
@@ -363,6 +361,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
api.Queue(req);
}
+ public void Close(Room room)
+ {
+ Debug.Assert(room.RoomID != null);
+
+ var request = new ClosePlaylistRequest(room.RoomID.Value);
+ request.Success += RefreshRooms;
+ api.Queue(request);
+ }
+
///
/// Push a room as a new subscreen.
///
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
index 16462b90c1..8988c82dee 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
@@ -220,8 +220,6 @@ namespace osu.Game.Screens.OnlinePlay
protected abstract string ScreenTitle { get; }
- protected virtual RoomManager CreateRoomManager() => new RoomManager();
-
protected abstract LoungeSubScreen CreateLounge();
ScreenStack IHasSubScreenStack.SubScreenStack => screenStack;
diff --git a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs
index efd0b80ebf..262816ae89 100644
--- a/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/IMultiplayerTestSceneDependencies.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.
-using osu.Game.Screens.OnlinePlay;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Tests.Visual.Spectator;
@@ -17,11 +16,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
///
TestMultiplayerClient MultiplayerClient { get; }
- ///
- /// The cached .
- ///
- new TestMultiplayerRoomManager RoomManager { get; }
-
///
/// The cached .
///
diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
index dca1fc8f3c..d1497d5142 100644
--- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
@@ -17,7 +17,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
public const int PLAYER_2_ID = 56;
public TestMultiplayerClient MultiplayerClient => OnlinePlayDependencies.MultiplayerClient;
- public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager;
public TestSpectatorClient SpectatorClient => OnlinePlayDependencies.SpectatorClient;
protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies;
@@ -56,7 +55,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("join room", () =>
{
SelectedRoom.Value = CreateRoom();
- API.Queue(new CreateRoomRequest(SelectedRoom.Value));
+ MultiplayerClient.CreateRoom(SelectedRoom.Value).ConfigureAwait(false);
});
AddUntilStep("wait for room join", () => RoomJoined);
diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs
index 88202d4327..24c33f2f49 100644
--- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestSceneDependencies.cs
@@ -3,7 +3,6 @@
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
-using osu.Game.Screens.OnlinePlay;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Tests.Visual.Spectator;
@@ -16,19 +15,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public TestMultiplayerClient MultiplayerClient { get; }
public TestSpectatorClient SpectatorClient { get; }
- public new TestMultiplayerRoomManager RoomManager => (TestMultiplayerRoomManager)base.RoomManager;
public MultiplayerTestSceneDependencies()
{
- MultiplayerClient = new TestMultiplayerClient(RoomManager);
+ MultiplayerClient = new TestMultiplayerClient(RequestsHandler);
SpectatorClient = CreateSpectatorClient();
CacheAs(MultiplayerClient);
CacheAs(SpectatorClient);
}
- protected override IRoomManager CreateRoomManager() => new TestMultiplayerRoomManager(RequestsHandler);
-
protected virtual TestSpectatorClient CreateSpectatorClient() => new TestSpectatorClient();
}
}
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 70e298f3e0..d514fc0d7e 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -10,6 +10,7 @@ using MessagePack;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
+using osu.Game.Beatmaps;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
@@ -17,6 +18,7 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
+using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -65,15 +67,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Resolved]
private IAPIProvider api { get; set; } = null!;
- private readonly TestMultiplayerRoomManager roomManager;
-
private MultiplayerPlaylistItem? currentItem => ServerRoom?.Playlist[currentIndex];
private int currentIndex;
private long lastPlaylistItemId;
- public TestMultiplayerClient(TestMultiplayerRoomManager roomManager)
+ private readonly TestRoomRequestsHandler apiRequestHandler;
+
+ public TestMultiplayerClient(TestRoomRequestsHandler? apiRequestHandler = null)
{
- this.roomManager = roomManager;
+ this.apiRequestHandler = apiRequestHandler ?? new TestRoomRequestsHandler();
}
public void Connect() => isConnected.Value = true;
@@ -214,7 +216,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
roomId = clone(roomId);
password = clone(password);
- ServerAPIRoom = roomManager.ServerSideRooms.Single(r => r.RoomID == roomId);
+ ServerAPIRoom = ServerSideRooms.Single(r => r.RoomID == roomId);
if (password != ServerAPIRoom.Password)
throw new InvalidOperationException("Invalid password.");
@@ -485,7 +487,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected override Task CreateRoom(MultiplayerRoom room)
{
- throw new NotImplementedException();
+ Room apiRoom = new Room(room)
+ {
+ Type = room.Settings.MatchType == MatchType.Playlists
+ ? MatchType.HeadToHead
+ : room.Settings.MatchType
+ };
+
+ AddServerSideRoom(apiRoom, api.LocalUser.Value);
+ return JoinRoom(apiRoom.RoomID!.Value, room.Settings.Password);
}
private async Task changeMatchType(MatchType type)
@@ -680,5 +690,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
isConnected.Value = false;
return Task.CompletedTask;
}
+
+ #region API Room Handling
+
+ public IReadOnlyList ServerSideRooms
+ => apiRequestHandler.ServerSideRooms;
+
+ public void AddServerSideRoom(Room room, APIUser host)
+ => apiRequestHandler.AddServerSideRoom(room, host);
+
+ public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager beatmapManager)
+ => apiRequestHandler.HandleRequest(request, localUser, beatmapManager);
+
+ #endregion
}
}
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
deleted file mode 100644
index 59ac9a9749..0000000000
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.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 System.Collections.Generic;
-using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Rooms;
-using osu.Game.Screens.OnlinePlay.Components;
-using osu.Game.Tests.Visual.OnlinePlay;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- ///
- /// A for use in multiplayer test scenes.
- /// Should generally not be used by itself outside of a .
- ///
- public partial class TestMultiplayerRoomManager : RoomManager
- {
- private readonly TestRoomRequestsHandler requestsHandler;
-
- public TestMultiplayerRoomManager(TestRoomRequestsHandler requestsHandler)
- {
- this.requestsHandler = requestsHandler;
- }
-
- public IReadOnlyList ServerSideRooms => requestsHandler.ServerSideRooms;
-
- ///
- /// Adds a room to a local "server-side" list that's returned when a is fired.
- ///
- /// The room.
- /// The host.
- public void AddServerSideRoom(Room room, APIUser host) => requestsHandler.AddServerSideRoom(room, host);
- }
-}
diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs
index e2670c9ad8..203922c057 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
RequestsHandler = new TestRoomRequestsHandler();
OngoingOperationTracker = new OngoingOperationTracker();
AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
- RoomManager = CreateRoomManager();
+ RoomManager = new TestRoomManager();
UserLookupCache = new TestUserLookupCache();
BeatmapLookupCache = new BeatmapLookupCache();
@@ -80,7 +80,5 @@ namespace osu.Game.Tests.Visual.OnlinePlay
if (instance is Drawable drawable)
drawableComponents.Add(drawable);
}
-
- protected virtual IRoomManager CreateRoomManager() => new TestRoomManager();
}
}
diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs
index 60d169a46f..bff2753929 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs
@@ -22,8 +22,14 @@ namespace osu.Game.Tests.Visual.OnlinePlay
[Resolved]
private IAPIProvider api { get; set; } = null!;
+ [Resolved]
+ private RulesetStore rulesets { get; set; } = null!;
+
public void AddRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false)
{
+ // Can't reference Osu ruleset project here.
+ ruleset ??= rulesets.GetRuleset(0)!;
+
for (int i = 0; i < count; i++)
{
AddRoom(new Room
@@ -33,12 +39,8 @@ namespace osu.Game.Tests.Visual.OnlinePlay
Duration = TimeSpan.FromSeconds(10),
Category = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal,
Password = withPassword ? @"password" : null,
- PlaylistItemStats = ruleset == null
- ? null
- : new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] },
- Playlist = ruleset == null
- ? Array.Empty()
- : [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }]
+ PlaylistItemStats = new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] },
+ Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }]
});
}
}
@@ -46,7 +48,11 @@ namespace osu.Game.Tests.Visual.OnlinePlay
public void AddRoom(Room room)
{
room.RoomID = -currentRoomId;
- api.Queue(new CreateRoomRequest(room));
+
+ var req = new CreateRoomRequest(room);
+ req.Success += AddOrUpdateRoom;
+ api.Queue(req);
+
currentRoomId++;
}
}
From 7c38089c7559350de5080cdad9b55d0e5165d41b Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Thu, 23 Jan 2025 16:22:52 +0900
Subject: [PATCH 055/262] Rename methods
---
.../Online/Multiplayer/MultiplayerClient.cs | 37 +++++++------
.../Multiplayer/OnlineMultiplayerClient.cs | 54 +++++++++----------
.../Multiplayer/TestMultiplayerClient.cs | 6 +--
3 files changed, 50 insertions(+), 47 deletions(-)
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 7dfe974651..a8f314d372 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -171,7 +171,7 @@ namespace osu.Game.Online.Multiplayer
throw new InvalidOperationException("Cannot join a multiplayer room while already in one.");
var cancellationSource = joinCancellationSource = new CancellationTokenSource();
- await initRoom(room, r => CreateRoom(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false);
+ await initRoom(room, r => CreateRoomInternal(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false);
}
///
@@ -187,7 +187,7 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(room.RoomID != null);
var cancellationSource = joinCancellationSource = new CancellationTokenSource();
- await initRoom(room, r => JoinRoom(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false);
+ await initRoom(room, r => JoinRoomInternal(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false);
}
private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken)
@@ -236,21 +236,6 @@ namespace osu.Game.Online.Multiplayer
{
}
- ///
- /// Creates the with the given settings.
- ///
- /// The room.
- /// The joined
- protected abstract Task CreateRoom(MultiplayerRoom room);
-
- ///
- /// Joins the with a given ID.
- ///
- /// The room ID.
- /// An optional password to use when joining the room.
- /// The joined .
- protected abstract Task JoinRoom(long roomId, string? password = null);
-
public Task LeaveRoom()
{
if (Room == null)
@@ -279,6 +264,24 @@ namespace osu.Game.Online.Multiplayer
});
}
+ ///
+ /// Creates the with the given settings.
+ ///
+ /// The room.
+ /// The joined
+ protected abstract Task CreateRoomInternal(MultiplayerRoom room);
+
+ ///
+ /// Joins the with a given ID.
+ ///
+ /// The room ID.
+ /// An optional password to use when joining the room.
+ /// The joined .
+ protected abstract Task JoinRoomInternal(long roomId, string? password = null);
+
+ ///
+ /// Leaves the currently-joined .
+ ///
protected abstract Task LeaveRoomInternal();
public abstract Task InvitePlayer(int userId);
diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
index 05f3e44405..068ba27789 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -75,7 +75,32 @@ namespace osu.Game.Online.Multiplayer
}
}
- protected override async Task JoinRoom(long roomId, string? password = null)
+ protected override async Task CreateRoomInternal(MultiplayerRoom room)
+ {
+ if (!IsConnected.Value)
+ throw new OperationCanceledException();
+
+ Debug.Assert(connection != null);
+
+ try
+ {
+ return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room).ConfigureAwait(false);
+ }
+ catch (HubException exception)
+ {
+ if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
+ {
+ Debug.Assert(connector != null);
+
+ await connector.Reconnect().ConfigureAwait(false);
+ return await CreateRoomInternal(room).ConfigureAwait(false);
+ }
+
+ throw;
+ }
+ }
+
+ protected override async Task JoinRoomInternal(long roomId, string? password = null)
{
if (!IsConnected.Value)
throw new OperationCanceledException();
@@ -93,7 +118,7 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(connector != null);
await connector.Reconnect().ConfigureAwait(false);
- return await JoinRoom(roomId, password).ConfigureAwait(false);
+ return await JoinRoomInternal(roomId, password).ConfigureAwait(false);
}
throw;
@@ -266,31 +291,6 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId);
}
- protected override async Task CreateRoom(MultiplayerRoom room)
- {
- if (!IsConnected.Value)
- throw new OperationCanceledException();
-
- Debug.Assert(connection != null);
-
- try
- {
- return await connection.InvokeAsync(nameof(IMultiplayerServer.CreateRoom), room).ConfigureAwait(false);
- }
- catch (HubException exception)
- {
- if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
- {
- Debug.Assert(connector != null);
-
- await connector.Reconnect().ConfigureAwait(false);
- return await CreateRoom(room).ConfigureAwait(false);
- }
-
- throw;
- }
- }
-
public override Task DisconnectInternal()
{
if (connector == null)
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index d514fc0d7e..359b223ad2 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -208,7 +208,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
((IMultiplayerClient)this).UserBeatmapAvailabilityChanged(clone(userId), clone(user.BeatmapAvailability));
}
- protected override async Task JoinRoom(long roomId, string? password = null)
+ protected override async Task JoinRoomInternal(long roomId, string? password = null)
{
if (RoomJoined || ServerAPIRoom != null)
throw new InvalidOperationException("Already joined a room");
@@ -485,7 +485,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId));
- protected override Task CreateRoom(MultiplayerRoom room)
+ protected override Task CreateRoomInternal(MultiplayerRoom room)
{
Room apiRoom = new Room(room)
{
@@ -495,7 +495,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
};
AddServerSideRoom(apiRoom, api.LocalUser.Value);
- return JoinRoom(apiRoom.RoomID!.Value, room.Settings.Password);
+ return JoinRoomInternal(apiRoom.RoomID!.Value, room.Settings.Password);
}
private async Task changeMatchType(MatchType type)
From a198b0830affdab861037c0a90525946fa446b5d Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Thu, 23 Jan 2025 17:18:01 +0900
Subject: [PATCH 056/262] Add comment indicating RoomManager shouldn't exist
---
osu.Game/Screens/OnlinePlay/Components/RoomManager.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
index 3abb4098fb..a1b61ea7a3 100644
--- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
@@ -13,6 +13,7 @@ using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Components
{
+ // Todo: This class should be inlined into the lounge.
public partial class RoomManager : Component, IRoomManager
{
public event Action? RoomsUpdated;
From f2d8ea299777ad6168eb90d04a574d10bf083837 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Thu, 23 Jan 2025 18:25:55 +0900
Subject: [PATCH 057/262] Fix incorrect continuation
---
.../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
index 279b140d36..72b581eac1 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
@@ -472,7 +472,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
client.CreateRoom(room).ContinueWith(t => Schedule(() =>
{
- if (t.IsCompleted)
+ if (t.IsCompletedSuccessfully)
onSuccess(room);
else if (t.IsFaulted)
{
From 6dbf466009f6ab12f2613eebb970a2a1d1e101b3 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Thu, 23 Jan 2025 18:30:11 +0900
Subject: [PATCH 058/262] Fix incorrect exception handling
In particular, when the exception is:
`AggregateException { AggregateException { HubException } }`,
then the existing code will only unwrap the first aggregate exception.
The overlay's code was copied from the extension so both have been
adjusted here.
---
.../Online/Multiplayer/MultiplayerClientExtensions.cs | 9 +++------
.../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 8 ++------
2 files changed, 5 insertions(+), 12 deletions(-)
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs
index d846e7f566..1cc5a8e70a 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs
@@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
+using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Logging;
namespace osu.Game.Online.Multiplayer
@@ -16,12 +17,8 @@ namespace osu.Game.Online.Multiplayer
{
if (t.IsFaulted)
{
- Exception? exception = t.Exception;
-
- if (exception is AggregateException ae)
- exception = ae.InnerException;
-
- Debug.Assert(exception != null);
+ Debug.Assert(t.Exception != null);
+ Exception exception = t.Exception.AsSingular();
if (exception.GetHubExceptionMessage() is string message)
// Hub exceptions generally contain something we can show the user directly.
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
index 72b581eac1..2a5a83fadf 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
@@ -476,12 +476,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
onSuccess(room);
else if (t.IsFaulted)
{
- Exception? exception = t.Exception;
-
- if (exception is AggregateException ae)
- exception = ae.InnerException;
-
- Debug.Assert(exception != null);
+ Debug.Assert(t.Exception != null);
+ Exception exception = t.Exception.AsSingular();
if (exception.GetHubExceptionMessage() is string message)
onError(message);
From e9d6411e615ba85a2989511a9f374682b20d25cf Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Thu, 23 Jan 2025 19:10:11 +0900
Subject: [PATCH 059/262] Clean up error handling
---
.../Match/MultiplayerMatchSettingsOverlay.cs | 58 +++++++++----------
1 file changed, 27 insertions(+), 31 deletions(-)
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
index 2a5a83fadf..eda3bace40 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
@@ -463,9 +463,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
.ContinueWith(t => Schedule(() =>
{
if (t.IsCompletedSuccessfully)
- onSuccess(room);
+ onSuccess();
else
- onError(t.Exception?.AsSingular().Message ?? "Error changing settings.");
+ onError(t.Exception, "Error changing settings");
}));
}
else
@@ -473,26 +473,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
client.CreateRoom(room).ContinueWith(t => Schedule(() =>
{
if (t.IsCompletedSuccessfully)
- onSuccess(room);
- else if (t.IsFaulted)
- {
- Debug.Assert(t.Exception != null);
- Exception exception = t.Exception.AsSingular();
-
- if (exception.GetHubExceptionMessage() is string message)
- onError(message);
- else
- onError($"Error creating room: {exception}");
- }
+ onSuccess();
else
- onError("Error creating room.");
+ onError(t.Exception, "Error creating room");
}));
}
}
private void hideError() => ErrorText.FadeOut(50);
- private void onSuccess(Room room) => Schedule(() =>
+ private void onSuccess() => Schedule(() =>
{
Debug.Assert(applyingSettingsOperation != null);
@@ -502,28 +492,34 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
applyingSettingsOperation = null;
});
- private void onError(string text) => Schedule(() =>
+ private void onError(Exception? exception, string description)
{
- Debug.Assert(applyingSettingsOperation != null);
+ if (exception is AggregateException aggregateException)
+ exception = aggregateException.AsSingular();
- // see https://github.com/ppy/osu-web/blob/2c97aaeb64fb4ed97c747d8383a35b30f57428c7/app/Models/Multiplayer/PlaylistItem.php#L48.
- const string not_found_prefix = "beatmaps not found:";
+ string message = exception?.GetHubExceptionMessage() ?? $"{description} ({exception?.Message})";
- if (text.StartsWith(not_found_prefix, StringComparison.Ordinal))
+ Schedule(() =>
{
- ErrorText.Text = "The selected beatmap is not available online.";
- room.Playlist.SingleOrDefault()?.MarkInvalid();
- }
- else
- {
- ErrorText.Text = text;
- }
+ Debug.Assert(applyingSettingsOperation != null);
- ErrorText.FadeIn(50);
+ // see https://github.com/ppy/osu-web/blob/2c97aaeb64fb4ed97c747d8383a35b30f57428c7/app/Models/Multiplayer/PlaylistItem.php#L48.
+ const string not_found_prefix = "beatmaps not found:";
- applyingSettingsOperation.Dispose();
- applyingSettingsOperation = null;
- });
+ if (message.StartsWith(not_found_prefix, StringComparison.Ordinal))
+ {
+ ErrorText.Text = "The selected beatmap is not available online.";
+ room.Playlist.SingleOrDefault()?.MarkInvalid();
+ }
+ else
+ ErrorText.Text = message;
+
+ ErrorText.FadeIn(50);
+
+ applyingSettingsOperation.Dispose();
+ applyingSettingsOperation = null;
+ });
+ }
protected override void Dispose(bool isDisposing)
{
From 8f17a44976439ba30c8ee13f1200d72821847c5a Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Thu, 23 Jan 2025 10:29:04 +0000
Subject: [PATCH 060/262] Remove unused default value
---
.../Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs
index 4f7023059f..b77176b49d 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
/// The ratio of between this and the previous . In the
/// case where one or both of the is undefined, this will have a value of 1.
///
- public double HitObjectIntervalRatio = 1;
+ public double HitObjectIntervalRatio;
///
public double Interval { get; private set; }
From ab4162e2aafc4e246ba070870e4967ab7a6e00cb Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Sat, 25 Jan 2025 19:27:21 +0900
Subject: [PATCH 061/262] Various refactorings and cleanups
---
.../TestSceneMultiplayerLoungeSubScreen.cs | 28 +++++--------------
.../TestScenePlaylistsLoungeSubScreen.cs | 28 +++----------------
.../Multiplayer/IMultiplayerLoungeServer.cs | 5 ++++
.../Online/Multiplayer/MultiplayerClient.cs | 3 +-
osu.Game/Online/Rooms/CreateRoomRequest.cs | 2 ++
osu.Game/Online/Rooms/JoinRoomRequest.cs | 2 ++
.../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 2 +-
.../OnlinePlay/Lounge/IOnlinePlayLounge.cs | 6 ++--
.../OnlinePlay/Lounge/LoungeSubScreen.cs | 6 ++--
.../Screens/OnlinePlay/Match/RoomSubScreen.cs | 3 ++
.../Multiplayer/MultiplayerLoungeSubScreen.cs | 2 +-
.../Playlists/PlaylistsLoungeSubScreen.cs | 2 +-
12 files changed, 34 insertions(+), 55 deletions(-)
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
index 4a259149e2..eb649acd2d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestJoinRoomWithoutPassword()
{
- addRoom(false);
+ AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
@@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestPopoverHidesOnBackButton()
{
- addRoom(true);
+ AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
@@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestPopoverHidesOnLeavingScreen()
{
- addRoom(true);
+ AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
@@ -86,7 +86,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
- addRoom(true);
+ AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
@@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
- addRoom(true);
+ AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
@@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
- addRoom(true);
+ AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
@@ -139,7 +139,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
- addRoom(true);
+ AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
@@ -149,20 +149,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("room joined", () => MultiplayerClient.RoomJoined);
}
- private void addRoom(bool withPassword)
- {
- int initialRoomCount = 0;
-
- AddStep("add room", () =>
- {
- initialRoomCount = roomsContainer.Rooms.Count;
- RoomManager.AddRooms(1, withPassword: withPassword);
- loungeScreen.RefreshRooms();
- });
-
- AddUntilStep("wait for room to appear", () => roomsContainer.Rooms.Count == initialRoomCount + 1);
- }
-
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies();
}
}
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
index 0897a3b2f5..53c7873de5 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
@@ -35,12 +35,7 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestManyRooms()
{
- AddStep("add rooms", () =>
- {
- RoomManager.AddRooms(500);
- loungeScreen.RefreshRooms();
- });
-
+ AddStep("add rooms", () => RoomManager.AddRooms(500));
AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 500);
}
@@ -49,12 +44,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left));
- AddStep("add rooms", () =>
- {
- RoomManager.AddRooms(30);
- loungeScreen.RefreshRooms();
- });
-
+ AddStep("add rooms", () => RoomManager.AddRooms(30));
AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30);
AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0]));
@@ -71,12 +61,7 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestScrollSelectedIntoView()
{
- AddStep("add rooms", () =>
- {
- RoomManager.AddRooms(30);
- loungeScreen.RefreshRooms();
- });
-
+ AddStep("add rooms", () => RoomManager.AddRooms(30));
AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30);
AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0]));
@@ -90,12 +75,7 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestEnteringRoomTakesLeaseOnSelection()
{
- AddStep("add rooms", () =>
- {
- RoomManager.AddRooms(1);
- loungeScreen.RefreshRooms();
- });
-
+ AddStep("add rooms", () => RoomManager.AddRooms(1));
AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 1);
AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled);
diff --git a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs
index c5eb6f9b36..0ee9fa54cd 100644
--- a/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs
+++ b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs
@@ -10,6 +10,11 @@ namespace osu.Game.Online.Multiplayer
///
public interface IMultiplayerLoungeServer
{
+ ///
+ /// Request to create a multiplayer room.
+ ///
+ /// The room to create.
+ /// The created multiplayer room.
Task CreateRoom(MultiplayerRoom room);
///
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index a8f314d372..6749ed9535 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -168,7 +168,7 @@ namespace osu.Game.Online.Multiplayer
public async Task CreateRoom(Room room)
{
if (Room != null)
- throw new InvalidOperationException("Cannot join a multiplayer room while already in one.");
+ throw new InvalidOperationException("Cannot create a multiplayer room while already in one.");
var cancellationSource = joinCancellationSource = new CancellationTokenSource();
await initRoom(room, r => CreateRoomInternal(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false);
@@ -212,6 +212,7 @@ namespace osu.Game.Online.Multiplayer
APIRoom.RoomID = joinedRoom.RoomID;
APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray();
APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
+ // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client.
APIRoom.EndDate = null;
Debug.Assert(LocalUser != null);
diff --git a/osu.Game/Online/Rooms/CreateRoomRequest.cs b/osu.Game/Online/Rooms/CreateRoomRequest.cs
index 9773bb5e7d..5b2ea77aad 100644
--- a/osu.Game/Online/Rooms/CreateRoomRequest.cs
+++ b/osu.Game/Online/Rooms/CreateRoomRequest.cs
@@ -15,6 +15,8 @@ namespace osu.Game.Online.Rooms
public CreateRoomRequest(Room room)
{
Room = room;
+
+ // Also copy back to the source model, since it is likely to have been stored elsewhere.
Success += r => Room.CopyFrom(r);
}
diff --git a/osu.Game/Online/Rooms/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs
index 13e7ac8c84..610e887242 100644
--- a/osu.Game/Online/Rooms/JoinRoomRequest.cs
+++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs
@@ -16,6 +16,8 @@ namespace osu.Game.Online.Rooms
{
Room = room;
Password = password;
+
+ // Also copy back to the source model, since it is likely to have been stored elsewhere.
Success += r => Room.CopyFrom(r);
}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs
index 032a231ad3..5de35ef101 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs
@@ -162,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
{
- lounge?.Clone(Room);
+ lounge?.OpenCopy(Room);
})
};
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs
index 8fa7d0751f..73ab84af13 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs
@@ -18,10 +18,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null);
///
- /// Clones the given room and opens it as a fresh (not-yet-created) one.
+ /// Copies the given room and opens it as a fresh (not-yet-created) one.
///
- /// The room to clone.
- void Clone(Room room);
+ /// The room to copy.
+ void OpenCopy(Room room);
///
/// Closes the given room.
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
index df17063fdf..0e08e398a4 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
@@ -309,7 +309,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
joiningRoomOperation = ongoingOperationTracker?.BeginOperation();
- TryJoin(room, password, r =>
+ JoinInternal(room, password, r =>
{
Open(room);
joiningRoomOperation?.Dispose();
@@ -323,9 +323,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
});
});
- protected abstract void TryJoin(Room room, string? password, Action onSuccess, Action onFailure);
+ protected abstract void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure);
- public void Clone(Room room)
+ public void OpenCopy(Room room)
{
Debug.Assert(room.RoomID != null);
diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
index d37f3b877c..80b3961f44 100644
--- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
@@ -353,6 +353,9 @@ namespace osu.Game.Screens.OnlinePlay.Match
return base.OnExiting(e);
}
+ ///
+ /// Parts from the current room.
+ ///
protected abstract void PartRoom();
private bool ensureExitConfirmed()
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
index e901ecbdce..873a9cde88 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent();
- protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure)
+ protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure)
{
client.JoinRoom(room, password).ContinueWith(result =>
{
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
index 92415e0eb1..6ed367328c 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
@@ -60,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return criteria;
}
- protected override void TryJoin(Room room, string? password, Action onSuccess, Action onFailure)
+ protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure)
{
var joinRoomRequest = new JoinRoomRequest(room, password);
From a7aa553445738068eb8075043cb64187ed6b73dc Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Sun, 26 Jan 2025 16:21:07 +0000
Subject: [PATCH 062/262] Fix incorrect `startTime` calculation
---
.../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
index 0c668797cd..486841b995 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
@@ -118,13 +118,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
noteObjects.Add(this);
}
- double startTime = hitObject.StartTime * clockRate;
-
// Retrieve the timing point at the note's start time
- TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime);
+ TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(hitObject.StartTime);
// Calculate the slider velocity at the note's start time.
- double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, startTime, clockRate);
+ double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, hitObject.StartTime, clockRate);
CurrentSliderVelocity = currentSliderVelocity;
EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity;
From 13c956c2482ee8ff81e83f283de9f17910ad189d Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Sun, 26 Jan 2025 20:15:13 +0000
Subject: [PATCH 063/262] Account for floating point errors
---
.../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
index 486841b995..f9ca2707ab 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
@@ -118,11 +118,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
noteObjects.Add(this);
}
+ // Using `hitObject.StartTime` causes floating point error differences
+ double normalizedStartTime = StartTime * clockRate;
+
// Retrieve the timing point at the note's start time
- TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(hitObject.StartTime);
+ TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalizedStartTime);
// Calculate the slider velocity at the note's start time.
- double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, hitObject.StartTime, clockRate);
+ double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalizedStartTime, clockRate);
CurrentSliderVelocity = currentSliderVelocity;
EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity;
From 71b89c390fe7d672ec8f1f61bbea31352315a4fb Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Mon, 27 Jan 2025 12:54:22 +0000
Subject: [PATCH 064/262] Rename class, rename children to hit objects and
groups, make fields un-settable
---
.../Difficulty/Evaluators/RhythmEvaluator.cs | 12 ++++----
.../Data/SamePatternsGroupedHitObjects.cs | 22 +++++++-------
...ects.cs => SameRhythmHitObjectGrouping.cs} | 30 +++++++++----------
.../Rhythm/TaikoDifficultyHitObjectRhythm.cs | 2 +-
.../TaikoRhythmDifficultyPreprocessor.cs | 10 +++----
5 files changed, 38 insertions(+), 38 deletions(-)
rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/{SameRhythmGroupedHitObjects.cs => SameRhythmHitObjectGrouping.cs} (65%)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
index 8accc6124c..f4686f2fe3 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
return difficulty;
}
- private static double evaluateDifficultyOf(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow)
+ private static double evaluateDifficultyOf(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow)
{
double intervalDifficulty = ratioDifficulty(sameRhythmGroupedHitObjects.HitObjectIntervalRatio);
double? previousInterval = sameRhythmGroupedHitObjects.Previous?.HitObjectInterval;
@@ -47,9 +47,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
intervalDifficulty *= repeatedIntervalPenalty(sameRhythmGroupedHitObjects, hitWindow);
// If a previous interval exists and there are multiple hit objects in the sequence:
- if (previousInterval != null && sameRhythmGroupedHitObjects.Children.Count > 1)
+ if (previousInterval != null && sameRhythmGroupedHitObjects.HitObjects.Count > 1)
{
- double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.Children.Count;
+ double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.HitObjects.Count;
double durationDifference = sameRhythmGroupedHitObjects.Duration - expectedDurationFromPrevious;
if (durationDifference > 0)
@@ -75,11 +75,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
///
/// Determines if the changes in hit object intervals is consistent based on a given threshold.
///
- private static double repeatedIntervalPenalty(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1)
+ private static double repeatedIntervalPenalty(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1)
{
double longIntervalPenalty = sameInterval(sameRhythmGroupedHitObjects, 3);
- double shortIntervalPenalty = sameRhythmGroupedHitObjects.Children.Count < 6
+ double shortIntervalPenalty = sameRhythmGroupedHitObjects.HitObjects.Count < 6
? sameInterval(sameRhythmGroupedHitObjects, 4)
: 1.0; // Returns a non-penalty if there are 6 or more notes within an interval.
@@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty;
- double sameInterval(SameRhythmGroupedHitObjects startObject, int intervalCount)
+ double sameInterval(SameRhythmHitObjectGrouping startObject, int intervalCount)
{
List intervals = new List();
var currentObject = startObject;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs
index cb22b2ef82..938cb4670f 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SamePatternsGroupedHitObjects.cs
@@ -7,34 +7,34 @@ using System.Linq;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
{
///
- /// Represents grouped by their 's interval.
+ /// Represents grouped by their 's interval.
///
public class SamePatternsGroupedHitObjects
{
- public IReadOnlyList Children { get; }
+ public IReadOnlyList Groups { get; }
public SamePatternsGroupedHitObjects? Previous { get; }
///
- /// The between children within this group.
- /// If there is only one child, this will have the value of the first child's .
+ /// The between groups .
+ /// If there is only one group, this will have the value of the first group's .
///
- public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval;
+ public double GroupInterval => Groups.Count > 1 ? Groups[1].Interval : Groups[0].Interval;
///
- /// The ratio of between this and the previous . In the
+ /// The ratio of between this and the previous . In the
/// case where there is no previous , this will have a value of 1.
///
- public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d;
+ public double IntervalRatio => GroupInterval / Previous?.GroupInterval ?? 1.0d;
- public TaikoDifficultyHitObject FirstHitObject => Children[0].FirstHitObject;
+ public TaikoDifficultyHitObject FirstHitObject => Groups[0].FirstHitObject;
- public IEnumerable AllHitObjects => Children.SelectMany(child => child.Children);
+ public IEnumerable AllHitObjects => Groups.SelectMany(hitObject => hitObject.HitObjects);
- public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List children)
+ public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List groups)
{
Previous = previous;
- Children = children;
+ Groups = groups;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs
similarity index 65%
rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs
rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs
index b77176b49d..9caa9b9958 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmGroupedHitObjects.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/Data/SameRhythmHitObjectGrouping.cs
@@ -10,46 +10,46 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
///
/// Represents a group of s with no rhythm variation.
///
- public class SameRhythmGroupedHitObjects : IHasInterval
+ public class SameRhythmHitObjectGrouping : IHasInterval
{
- public List Children { get; private set; }
+ public readonly List HitObjects;
- public TaikoDifficultyHitObject FirstHitObject => Children[0];
+ public TaikoDifficultyHitObject FirstHitObject => HitObjects[0];
- public SameRhythmGroupedHitObjects? Previous;
+ public readonly SameRhythmHitObjectGrouping? Previous;
///
/// of the first hit object.
///
- public double StartTime => Children[0].StartTime;
+ public double StartTime => HitObjects[0].StartTime;
///
/// The interval between the first and final hit object within this group.
///
- public double Duration => Children[^1].StartTime - Children[0].StartTime;
+ public double Duration => HitObjects[^1].StartTime - HitObjects[0].StartTime;
///
- /// The interval in ms of each hit object in this . This is only defined if there is
- /// more than two hit objects in this .
+ /// The interval in ms of each hit object in this . This is only defined if there is
+ /// more than two hit objects in this .
///
- public double? HitObjectInterval;
+ public readonly double? HitObjectInterval;
///
- /// The ratio of between this and the previous . In the
+ /// The ratio of between this and the previous . In the
/// case where one or both of the is undefined, this will have a value of 1.
///
- public double HitObjectIntervalRatio;
+ public readonly double HitObjectIntervalRatio;
///
- public double Interval { get; private set; }
+ public double Interval { get; }
- public SameRhythmGroupedHitObjects(SameRhythmGroupedHitObjects? previous, List children)
+ public SameRhythmHitObjectGrouping(SameRhythmHitObjectGrouping? previous, List hitObjects)
{
Previous = previous;
- Children = children;
+ HitObjects = hitObjects;
// Calculate the average interval between hitobjects, or null if there are fewer than two
- HitObjectInterval = Children.Count < 2 ? null : Duration / (Children.Count - 1);
+ HitObjectInterval = HitObjects.Count < 2 ? null : Duration / (HitObjects.Count - 1);
// Calculate the ratio between this group's interval and the previous group's interval
HitObjectIntervalRatio = Previous?.HitObjectInterval != null && HitObjectInterval != null
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs
index 351015ae08..3503a836fa 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
///
/// The group of hit objects with consistent rhythm that this object belongs to.
///
- public SameRhythmGroupedHitObjects? SameRhythmGroupedHitObjects;
+ public SameRhythmHitObjectGrouping? SameRhythmGroupedHitObjects;
///
/// The larger pattern of rhythm groups that this object is part of.
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
index cd56d835dc..3ebc0c25b7 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
foreach (var rhythmGroup in rhythmGroups)
{
- foreach (var hitObject in rhythmGroup.Children)
+ foreach (var hitObject in rhythmGroup.HitObjects)
{
hitObject.Rhythm.SameRhythmGroupedHitObjects = rhythmGroup;
hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval;
@@ -33,21 +33,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
}
}
- private static List createSameRhythmGroupedHitObjects(List hitObjects)
+ private static List createSameRhythmGroupedHitObjects(List hitObjects)
{
- var rhythmGroups = new List();
+ var rhythmGroups = new List();
var groups = IntervalGroupingUtils.GroupByInterval(hitObjects);
foreach (var group in groups)
{
var previous = rhythmGroups.Count > 0 ? rhythmGroups[^1] : null;
- rhythmGroups.Add(new SameRhythmGroupedHitObjects(previous, group));
+ rhythmGroups.Add(new SameRhythmHitObjectGrouping(previous, group));
}
return rhythmGroups;
}
- private static List createSamePatternGroupedHitObjects(List rhythmGroups)
+ private static List createSamePatternGroupedHitObjects(List rhythmGroups)
{
var patternGroups = new List();
var groups = IntervalGroupingUtils.GroupByInterval(rhythmGroups);
From f3c17f1c2b73e4f12fd00b130bd8326ca17a74e6 Mon Sep 17 00:00:00 2001
From: tsunyoku
Date: Mon, 27 Jan 2025 12:56:33 +0000
Subject: [PATCH 065/262] Use correct English
---
.../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
index f9ca2707ab..d6a2d5874e 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
@@ -119,13 +119,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
}
// Using `hitObject.StartTime` causes floating point error differences
- double normalizedStartTime = StartTime * clockRate;
+ double normalisedStartTime = StartTime * clockRate;
// Retrieve the timing point at the note's start time
- TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalizedStartTime);
+ TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalisedStartTime);
// Calculate the slider velocity at the note's start time.
- double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalizedStartTime, clockRate);
+ double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalisedStartTime, clockRate);
CurrentSliderVelocity = currentSliderVelocity;
EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity;
From 46144960e50fc49867bccaed2ee035e983a05718 Mon Sep 17 00:00:00 2001
From: "Rian (Reza Mouna Hendrian)"
<52914632+Rian8337@users.noreply.github.com>
Date: Thu, 30 Jan 2025 03:06:05 +0800
Subject: [PATCH 066/262] Remove unnecessary strain sorting in difficult slider
count (#31724)
---
osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index 89adda302c..6f1b680211 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -53,13 +53,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
if (sliderStrains.Count == 0)
return 0;
- double[] sortedStrains = sliderStrains.OrderDescending().ToArray();
-
- double maxSliderStrain = sortedStrains.Max();
+ double maxSliderStrain = sliderStrains.Max();
if (maxSliderStrain == 0)
return 0;
- return sortedStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0))));
+ return sliderStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0))));
}
}
}
From 2ee480c442436bb442b8b6171e2f42b86c3cbfa8 Mon Sep 17 00:00:00 2001
From: James Wilson
Date: Thu, 30 Jan 2025 13:58:38 +0000
Subject: [PATCH 067/262] Clamp `estimateImproperlyFollowedDifficultSliders`
between 0 and `attributes.AimDifficultSliderCount` (#31736)
---
osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index f191180630..dc2df39cdb 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
// We add tick misses here since they too mean that the player didn't follow the slider properly
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
- estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, attributes.AimDifficultSliderCount);
+ estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, attributes.AimDifficultSliderCount);
}
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / attributes.AimDifficultSliderCount, 3) + attributes.SliderFactor;
From fa844b0ebc783222beadd1e6889dada450823219 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Wed, 5 Feb 2025 15:01:59 +0900
Subject: [PATCH 068/262] Rename `Colour` / `Rhythm` related fields and classes
---
.../Difficulty/Evaluators/ColourEvaluator.cs | 18 +++++-----
.../Difficulty/Evaluators/RhythmEvaluator.cs | 12 +++----
.../Difficulty/Evaluators/StaminaEvaluator.cs | 4 +--
...yHitObjectColour.cs => TaikoColourData.cs} | 2 +-
.../TaikoColourDifficultyPreprocessor.cs | 10 +++---
...yHitObjectRhythm.cs => TaikoRhythmData.cs} | 35 +++++++++----------
.../TaikoRhythmDifficultyPreprocessor.cs | 4 +--
.../Preprocessing/TaikoDifficultyHitObject.cs | 8 ++---
.../Difficulty/Skills/Reading.cs | 2 +-
.../Difficulty/Skills/Stamina.cs | 2 +-
10 files changed, 48 insertions(+), 49 deletions(-)
rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/{TaikoDifficultyHitObjectColour.cs => TaikoColourData.cs} (96%)
rename osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/{TaikoDifficultyHitObjectRhythm.cs => TaikoRhythmData.cs} (75%)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
index 166c01f507..b715dfc37a 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
@@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1);
- double currentRatio = current.Rhythm.Ratio;
- double previousRatio = previousHitObject.Rhythm.Ratio;
+ double currentRatio = current.RhythmData.Ratio;
+ double previousRatio = previousHitObject.RhythmData.Ratio;
// A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error.
if (Math.Abs(1 - currentRatio / previousRatio) <= threshold)
@@ -61,17 +61,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
{
var taikoObject = (TaikoDifficultyHitObject)hitObject;
- TaikoDifficultyHitObjectColour colour = taikoObject.Colour;
+ TaikoColourData colourData = taikoObject.ColourData;
double difficulty = 0.0d;
- if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
- difficulty += evaluateMonoStreakDifficulty(colour.MonoStreak);
+ if (colourData.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
+ difficulty += evaluateMonoStreakDifficulty(colourData.MonoStreak);
- if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
- difficulty += evaluateAlternatingMonoPatternDifficulty(colour.AlternatingMonoPattern);
+ if (colourData.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
+ difficulty += evaluateAlternatingMonoPatternDifficulty(colourData.AlternatingMonoPattern);
- if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
- difficulty += evaluateRepeatingHitPatternsDifficulty(colour.RepeatingHitPattern);
+ if (colourData.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
+ difficulty += evaluateRepeatingHitPatternsDifficulty(colourData.RepeatingHitPattern);
double consistencyPenalty = consistentRatioPenalty(taikoObject);
difficulty *= consistencyPenalty;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
index f4686f2fe3..3b3aea07f3 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
@@ -18,21 +18,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
///
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow)
{
- TaikoDifficultyHitObjectRhythm rhythm = ((TaikoDifficultyHitObject)hitObject).Rhythm;
+ TaikoRhythmData rhythmData = ((TaikoDifficultyHitObject)hitObject).RhythmData;
double difficulty = 0.0d;
double sameRhythm = 0;
double samePattern = 0;
double intervalPenalty = 0;
- if (rhythm.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects
+ if (rhythmData.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects
{
- sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmGroupedHitObjects, hitWindow);
- intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmGroupedHitObjects, hitWindow);
+ sameRhythm += 10.0 * evaluateDifficultyOf(rhythmData.SameRhythmGroupedHitObjects, hitWindow);
+ intervalPenalty = repeatedIntervalPenalty(rhythmData.SameRhythmGroupedHitObjects, hitWindow);
}
- if (rhythm.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects
- samePattern += 1.15 * ratioDifficulty(rhythm.SamePatternsGroupedHitObjects.IntervalRatio);
+ if (rhythmData.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects
+ samePattern += 1.15 * ratioDifficulty(rhythmData.SamePatternsGroupedHitObjects.IntervalRatio);
difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
index a9884b2328..32ed8ec189 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
@@ -55,8 +55,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
///
private static int availableFingersFor(TaikoDifficultyHitObject hitObject)
{
- DifficultyHitObject? previousColourChange = hitObject.Colour.PreviousColourChange;
- DifficultyHitObject? nextColourChange = hitObject.Colour.NextColourChange;
+ DifficultyHitObject? previousColourChange = hitObject.ColourData.PreviousColourChange;
+ DifficultyHitObject? nextColourChange = hitObject.ColourData.NextColourChange;
if (previousColourChange != null && hitObject.StartTime - previousColourChange.StartTime < 300)
{
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs
similarity index 96%
rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs
rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs
index abf6fb3672..81201b6584 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourData.cs
@@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
///
/// Stores colour compression information for a .
///
- public class TaikoDifficultyHitObjectColour
+ public class TaikoColourData
{
///
/// The that encodes this note.
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs
index 18a299ae92..3c6ef7c53c 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs
@@ -14,8 +14,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
public static class TaikoColourDifficultyPreprocessor
{
///
- /// Processes and encodes a list of s into a list of s,
- /// assigning the appropriate s to each .
+ /// Processes and encodes a list of s into a list of s,
+ /// assigning the appropriate s to each .
///
public static void ProcessAndAssign(List hitObjects)
{
@@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
foreach (var hitObject in monoStreak.HitObjects)
{
- hitObject.Colour.RepeatingHitPattern = repeatingHitPattern;
- hitObject.Colour.AlternatingMonoPattern = monoPattern;
- hitObject.Colour.MonoStreak = monoStreak;
+ hitObject.ColourData.RepeatingHitPattern = repeatingHitPattern;
+ hitObject.ColourData.AlternatingMonoPattern = monoPattern;
+ hitObject.ColourData.MonoStreak = monoStreak;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs
similarity index 75%
rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs
rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs
index 3503a836fa..d895dcfc55 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs
@@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
///
/// Stores rhythm data for a .
///
- public class TaikoDifficultyHitObjectRhythm
+ public class TaikoRhythmData
{
///
/// The group of hit objects with consistent rhythm that this object belongs to.
@@ -39,25 +39,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
/// - speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch).
///
///
- private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms =
+ private static readonly TaikoRhythmData[] common_rhythms =
{
- new TaikoDifficultyHitObjectRhythm(1, 1),
- new TaikoDifficultyHitObjectRhythm(2, 1),
- new TaikoDifficultyHitObjectRhythm(1, 2),
- new TaikoDifficultyHitObjectRhythm(3, 1),
- new TaikoDifficultyHitObjectRhythm(1, 3),
- new TaikoDifficultyHitObjectRhythm(3, 2),
- new TaikoDifficultyHitObjectRhythm(2, 3),
- new TaikoDifficultyHitObjectRhythm(5, 4),
- new TaikoDifficultyHitObjectRhythm(4, 5)
+ new TaikoRhythmData(1, 1),
+ new TaikoRhythmData(2, 1),
+ new TaikoRhythmData(1, 2),
+ new TaikoRhythmData(3, 1),
+ new TaikoRhythmData(1, 3),
+ new TaikoRhythmData(3, 2),
+ new TaikoRhythmData(2, 3),
+ new TaikoRhythmData(5, 4),
+ new TaikoRhythmData(4, 5)
};
///
- /// Initialises a new instance of s,
+ /// Initialises a new instance of s,
/// calculating the closest rhythm change and its associated difficulty for the current hit object.
///
/// The current being processed.
- public TaikoDifficultyHitObjectRhythm(TaikoDifficultyHitObject current)
+ public TaikoRhythmData(TaikoDifficultyHitObject current)
{
var previous = current.Previous(0);
@@ -67,8 +67,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
return;
}
- TaikoDifficultyHitObjectRhythm closestRhythm = getClosestRhythm(current.DeltaTime, previous.DeltaTime);
- Ratio = closestRhythm.Ratio;
+ TaikoRhythmData closestRhythmData = getClosestRhythm(current.DeltaTime, previous.DeltaTime);
+ Ratio = closestRhythmData.Ratio;
}
///
@@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
///
/// The numerator for .
/// The denominator for
- private TaikoDifficultyHitObjectRhythm(int numerator, int denominator)
+ private TaikoRhythmData(int numerator, int denominator)
{
Ratio = numerator / (double)denominator;
}
@@ -88,11 +88,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
/// The time difference between the current hit object and the previous one.
/// The time difference between the previous hit object and the one before it.
/// The closest matching rhythm from .
- private TaikoDifficultyHitObjectRhythm getClosestRhythm(double currentDeltaTime, double previousDeltaTime)
+ private TaikoRhythmData getClosestRhythm(double currentDeltaTime, double previousDeltaTime)
{
double ratio = currentDeltaTime / previousDeltaTime;
return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
}
}
}
-
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
index 3ebc0c25b7..45cc29c99e 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
{
foreach (var hitObject in rhythmGroup.HitObjects)
{
- hitObject.Rhythm.SameRhythmGroupedHitObjects = rhythmGroup;
+ hitObject.RhythmData.SameRhythmGroupedHitObjects = rhythmGroup;
hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval;
}
}
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
{
foreach (var hitObject in patternGroup.AllHitObjects)
{
- hitObject.Rhythm.SamePatternsGroupedHitObjects = patternGroup;
+ hitObject.RhythmData.SamePatternsGroupedHitObjects = patternGroup;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
index d6a2d5874e..5c5503c25d 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
///
/// The rhythm required to hit this hit object.
///
- public readonly TaikoDifficultyHitObjectRhythm Rhythm;
+ public readonly TaikoRhythmData RhythmData;
///
/// The interval between this hit object and the surrounding hit objects in its rhythm group.
@@ -52,7 +52,7 @@ 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.
///
- public readonly TaikoDifficultyHitObjectColour Colour;
+ public readonly TaikoColourData ColourData;
///
/// The adjusted BPM of this hit object, based on its slider velocity and scroll speed.
@@ -92,10 +92,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
noteDifficultyHitObjects = noteObjects;
// Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor
- Colour = new TaikoDifficultyHitObjectColour();
+ ColourData = new TaikoColourData();
// Create a Rhythm object, its properties are filled in by TaikoDifficultyHitObjectRhythm
- Rhythm = new TaikoDifficultyHitObjectRhythm(this);
+ RhythmData = new TaikoRhythmData(this);
switch ((hitObject as Hit)?.Type)
{
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs
index 885131404a..7be1107b70 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Reading.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
}
var taikoObject = (TaikoDifficultyHitObject)current;
- int index = taikoObject.Colour.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0;
+ int index = taikoObject.ColourData.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0;
currentStrain *= DifficultyCalculationUtils.Logistic(index, 4, -1 / 25.0, 0.5) + 0.5;
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
index 12e1396dd7..0e1f3d41cf 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
// Safely prevents previous strains from shifting as new notes are added.
var currentObject = current as TaikoDifficultyHitObject;
- int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0;
+ int index = currentObject?.ColourData.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0;
double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30);
From 709ad02a517606b07b6a4aaf3f55e611a94219c9 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Wed, 5 Feb 2025 15:09:51 +0900
Subject: [PATCH 069/262] Simplify `TaikoRhythmData`'s ratio computation
---
.../Preprocessing/Rhythm/TaikoRhythmData.cs | 68 +++++++------------
1 file changed, 25 insertions(+), 43 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs
index d895dcfc55..6c4a332624 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs
@@ -27,30 +27,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
/// to previous for the rhythm change.
/// A above 1 indicates a slow-down; a below 1 indicates a speed-up.
///
- public readonly double Ratio;
-
- ///
- /// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object.
- ///
///
- /// The general guidelines for the values are:
- ///
- /// - rhythm changes with ratio closer to 1 (that are not 1) are harder to play,
- /// - speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch).
- ///
+ /// This is snapped to the closest matching .
///
- private static readonly TaikoRhythmData[] common_rhythms =
- {
- new TaikoRhythmData(1, 1),
- new TaikoRhythmData(2, 1),
- new TaikoRhythmData(1, 2),
- new TaikoRhythmData(3, 1),
- new TaikoRhythmData(1, 3),
- new TaikoRhythmData(3, 2),
- new TaikoRhythmData(2, 3),
- new TaikoRhythmData(5, 4),
- new TaikoRhythmData(4, 5)
- };
+ public readonly double Ratio;
///
/// Initialises a new instance of s,
@@ -67,31 +47,33 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
return;
}
- TaikoRhythmData closestRhythmData = getClosestRhythm(current.DeltaTime, previous.DeltaTime);
- Ratio = closestRhythmData.Ratio;
+ double actualRatio = current.DeltaTime / previous.DeltaTime;
+ double closestRatio = common_ratios.OrderBy(r => Math.Abs(r - actualRatio)).First();
+
+ Ratio = closestRatio;
}
///
- /// Creates an object representing a rhythm change.
+ /// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object.
///
- /// The numerator for .
- /// The denominator for
- private TaikoRhythmData(int numerator, int denominator)
+ ///
+ /// The general guidelines for the values are:
+ ///
+ /// - rhythm changes with ratio closer to 1 (that are not 1) are harder to play,
+ /// - speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch).
+ ///
+ ///
+ private static readonly double[] common_ratios = new[]
{
- Ratio = numerator / (double)denominator;
- }
-
- ///
- /// Determines the closest rhythm change from that matches the timing ratio
- /// between the current and previous intervals.
- ///
- /// The time difference between the current hit object and the previous one.
- /// The time difference between the previous hit object and the one before it.
- /// The closest matching rhythm from .
- private TaikoRhythmData getClosestRhythm(double currentDeltaTime, double previousDeltaTime)
- {
- double ratio = currentDeltaTime / previousDeltaTime;
- return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
- }
+ 1.0 / 1,
+ 2.0 / 1,
+ 1.0 / 2,
+ 3.0 / 1,
+ 1.0 / 3,
+ 3.0 / 2,
+ 2.0 / 3,
+ 5.0 / 4,
+ 4.0 / 5
+ };
}
}
From fc933902844ce21ffa6961920dd96bbe47d94fa1 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Wed, 5 Feb 2025 15:10:15 +0900
Subject: [PATCH 070/262] Remove unused `HitObjectInterval`
---
.../Rhythm/TaikoRhythmDifficultyPreprocessor.cs | 5 -----
.../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 5 -----
2 files changed, 10 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
index 45cc29c99e..8b126f85ce 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
@@ -16,10 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
foreach (var rhythmGroup in rhythmGroups)
{
foreach (var hitObject in rhythmGroup.HitObjects)
- {
hitObject.RhythmData.SameRhythmGroupedHitObjects = rhythmGroup;
- hitObject.HitObjectInterval = rhythmGroup.HitObjectInterval;
- }
}
var patternGroups = createSamePatternGroupedHitObjects(rhythmGroups);
@@ -27,9 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
foreach (var patternGroup in patternGroups)
{
foreach (var hitObject in patternGroup.AllHitObjects)
- {
hitObject.RhythmData.SamePatternsGroupedHitObjects = patternGroup;
- }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
index 5c5503c25d..489b36b259 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
@@ -43,11 +43,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
///
public readonly TaikoRhythmData RhythmData;
- ///
- /// The interval between this hit object and the surrounding hit objects in its rhythm group.
- ///
- public double? HitObjectInterval { get; set; }
-
///
/// 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.
From 325483192a26f41d7019c4cf28c22fe91da1f1e3 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Wed, 5 Feb 2025 15:13:04 +0900
Subject: [PATCH 071/262] Tidy up xmldoc and remove another unused field
---
.../Preprocessing/TaikoDifficultyHitObject.cs | 52 ++++++++-----------
.../Difficulty/TaikoDifficultyCalculator.cs | 1 -
.../Difficulty/Utils/IHasInterval.cs | 2 +-
3 files changed, 23 insertions(+), 32 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
index 489b36b259..f407e13ff1 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
@@ -6,6 +6,7 @@ using System.Linq;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
@@ -39,13 +40,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
public readonly int NoteIndex;
///
- /// The rhythm required to hit this hit object.
+ /// Rhythm data used by .
+ /// This is populated via .
///
public readonly TaikoRhythmData RhythmData;
///
- /// 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.
+ /// Colour data used by and .
+ /// This is populated via .
///
public readonly TaikoColourData ColourData;
@@ -54,19 +56,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
///
public double EffectiveBPM;
- ///
- /// The current slider velocity of this hit object.
- ///
- public double CurrentSliderVelocity;
-
- public double Interval => DeltaTime;
-
///
/// Creates a new difficulty hit object.
///
/// The gameplay associated with this difficulty object.
/// The gameplay preceding .
- /// The gameplay preceding .
/// The rate of the gameplay clock. Modified by speed-changing mods.
/// The list of all s in the current beatmap.
/// The list of centre (don) s in the current beatmap.
@@ -75,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
/// The position of this in the list.
/// The control point info of the beatmap.
/// The global slider velocity of the beatmap.
- public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate,
+ public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate,
List objects,
List centreHitObjects,
List rimHitObjects,
@@ -86,29 +80,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
{
noteDifficultyHitObjects = noteObjects;
- // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor
ColourData = new TaikoColourData();
-
- // Create a Rhythm object, its properties are filled in by TaikoDifficultyHitObjectRhythm
RhythmData = new TaikoRhythmData(this);
- switch ((hitObject as Hit)?.Type)
+ if (hitObject is Hit hit)
{
- case HitType.Centre:
- MonoIndex = centreHitObjects.Count;
- centreHitObjects.Add(this);
- monoDifficultyHitObjects = centreHitObjects;
- break;
+ switch (hit.Type)
+ {
+ case HitType.Centre:
+ MonoIndex = centreHitObjects.Count;
+ centreHitObjects.Add(this);
+ monoDifficultyHitObjects = centreHitObjects;
+ break;
- case HitType.Rim:
- MonoIndex = rimHitObjects.Count;
- rimHitObjects.Add(this);
- monoDifficultyHitObjects = rimHitObjects;
- break;
- }
+ case HitType.Rim:
+ MonoIndex = rimHitObjects.Count;
+ rimHitObjects.Add(this);
+ monoDifficultyHitObjects = rimHitObjects;
+ break;
+ }
- if (hitObject is Hit)
- {
NoteIndex = noteObjects.Count;
noteObjects.Add(this);
}
@@ -121,7 +112,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
// Calculate the slider velocity at the note's start time.
double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalisedStartTime, clockRate);
- CurrentSliderVelocity = currentSliderVelocity;
EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity;
}
@@ -142,5 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
public TaikoDifficultyHitObject? PreviousNote(int backwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex - (backwardsIndex + 1));
public TaikoDifficultyHitObject? NextNote(int forwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex + (forwardsIndex + 1));
+
+ public double Interval => DeltaTime;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index acd654f9b8..6b9986bd68 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -78,7 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
difficultyHitObjects.Add(new TaikoDifficultyHitObject(
beatmap.HitObjects[i],
beatmap.HitObjects[i - 1],
- beatmap.HitObjects[i - 2],
clockRate,
difficultyHitObjects,
centreObjects,
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs
index 8f80bb6079..a42940180c 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IHasInterval.cs
@@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils
public interface IHasInterval
{
///
- /// The interval between 2 objects start times.
+ /// The interval – ie delta time – between this object and a known previous object.
///
double Interval { get; }
}
From 8447679db9f038b5ddfefbe7337d87ea38000c22 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Wed, 5 Feb 2025 15:41:31 +0900
Subject: [PATCH 072/262] Initial tidy-up pass on `IntervalGroupingUtils`
---
.../TaikoRhythmDifficultyPreprocessor.cs | 17 +++-----
.../Difficulty/Utils/IntervalGroupingUtils.cs | 41 ++++++++-----------
2 files changed, 23 insertions(+), 35 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
index 8b126f85ce..5bc0fdbc03 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmDifficultyPreprocessor.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
using osu.Game.Rulesets.Taiko.Difficulty.Utils;
@@ -31,13 +32,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
private static List createSameRhythmGroupedHitObjects(List hitObjects)
{
var rhythmGroups = new List();
- var groups = IntervalGroupingUtils.GroupByInterval(hitObjects);
- foreach (var group in groups)
- {
- var previous = rhythmGroups.Count > 0 ? rhythmGroups[^1] : null;
- rhythmGroups.Add(new SameRhythmHitObjectGrouping(previous, group));
- }
+ foreach (var grouped in IntervalGroupingUtils.GroupByInterval(hitObjects))
+ rhythmGroups.Add(new SameRhythmHitObjectGrouping(rhythmGroups.LastOrDefault(), grouped));
return rhythmGroups;
}
@@ -45,13 +42,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
private static List createSamePatternGroupedHitObjects(List rhythmGroups)
{
var patternGroups = new List();
- var groups = IntervalGroupingUtils.GroupByInterval(rhythmGroups);
- foreach (var group in groups)
- {
- var previous = patternGroups.Count > 0 ? patternGroups[^1] : null;
- patternGroups.Add(new SamePatternsGroupedHitObjects(previous, group));
- }
+ foreach (var grouped in IntervalGroupingUtils.GroupByInterval(rhythmGroups))
+ patternGroups.Add(new SamePatternsGroupedHitObjects(patternGroups.LastOrDefault(), grouped));
return patternGroups;
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
index 3b6f5406b4..f04dec1c08 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
@@ -8,56 +8,51 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils
{
public static class IntervalGroupingUtils
{
- public static List> GroupByInterval(IReadOnlyList data, double marginOfError = 5) where T : IHasInterval
+ public static List> GroupByInterval(IReadOnlyList objects) where T : IHasInterval
{
var groups = new List>();
- if (data.Count == 0)
- return groups;
int i = 0;
-
- while (i < data.Count)
- {
- var group = createGroup(data, ref i, marginOfError);
- groups.Add(group);
- }
+ while (i < objects.Count)
+ groups.Add(createNextGroup(objects, ref i));
return groups;
}
- private static List createGroup(IReadOnlyList data, ref int i, double marginOfError) where T : IHasInterval
+ private static List createNextGroup(IReadOnlyList objects, ref int i) where T : IHasInterval
{
- var children = new List { data[i] };
+ const double margin_of_error = 5;
+
+ var groupedObjects = new List { objects[i] };
i++;
- for (; i < data.Count - 1; i++)
+ for (; i < objects.Count - 1; i++)
{
- // An interval change occured, add the current data if the next interval is larger.
- if (!Precision.AlmostEquals(data[i].Interval, data[i + 1].Interval, marginOfError))
+ // An interval change occured, add the current object if the next interval is larger.
+ if (!Precision.AlmostEquals(objects[i].Interval, objects[i + 1].Interval, margin_of_error))
{
- if (data[i + 1].Interval > data[i].Interval + marginOfError)
+ if (objects[i + 1].Interval > objects[i].Interval + margin_of_error)
{
- children.Add(data[i]);
+ groupedObjects.Add(objects[i]);
i++;
}
- return children;
+ return groupedObjects;
}
// No interval change occurred
- children.Add(data[i]);
+ groupedObjects.Add(objects[i]);
}
- // Check if the last two objects in the data form a "flat" rhythm pattern within the specified margin of error.
+ // Check if the last two objects in the object form a "flat" rhythm pattern within the specified margin of error.
// If true, add the current object to the group and increment the index to process the next object.
- if (data.Count > 2 && i < data.Count &&
- Precision.AlmostEquals(data[^1].Interval, data[^2].Interval, marginOfError))
+ if (objects.Count > 2 && i < objects.Count && Precision.AlmostEquals(objects[^1].Interval, objects[^2].Interval, margin_of_error))
{
- children.Add(data[i]);
+ groupedObjects.Add(objects[i]);
i++;
}
- return children;
+ return groupedObjects;
}
}
}
From 40ea7ff2383248c4e3cdbd2c042cf692792f7bd8 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Wed, 5 Feb 2025 18:48:48 +0900
Subject: [PATCH 073/262] Add better documentation for interval change code
---
.../Difficulty/Utils/IntervalGroupingUtils.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
index f04dec1c08..7bd7aa7677 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
@@ -28,9 +28,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils
for (; i < objects.Count - 1; i++)
{
- // An interval change occured, add the current object if the next interval is larger.
if (!Precision.AlmostEquals(objects[i].Interval, objects[i + 1].Interval, margin_of_error))
{
+ // When an interval change occurs, include the object with the differing interval in the case it increased
+ // See https://github.com/ppy/osu/pull/31636#discussion_r1942368372 for rationale.
if (objects[i + 1].Interval > objects[i].Interval + margin_of_error)
{
groupedObjects.Add(objects[i]);
From 4f6fd68a9195d170eeca5983e0a76d5e5fcc78b4 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 7 Feb 2025 13:54:35 +0900
Subject: [PATCH 074/262] Fix inspections
---
.../Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs
index 6c4a332624..247fb06dc0 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoRhythmData.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
}
double actualRatio = current.DeltaTime / previous.DeltaTime;
- double closestRatio = common_ratios.OrderBy(r => Math.Abs(r - actualRatio)).First();
+ double closestRatio = common_ratios.MinBy(r => Math.Abs(r - actualRatio));
Ratio = closestRatio;
}
@@ -63,8 +63,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
/// - speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch).
///
///
- private static readonly double[] common_ratios = new[]
- {
+ private static readonly double[] common_ratios =
+ [
1.0 / 1,
2.0 / 1,
1.0 / 2,
@@ -74,6 +74,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
2.0 / 3,
5.0 / 4,
4.0 / 5
- };
+ ];
}
}
From 9f90ebb2f774bd023befdc73a849fe087cca9550 Mon Sep 17 00:00:00 2001
From: James Wilson
Date: Fri, 7 Feb 2025 10:21:12 +0000
Subject: [PATCH 075/262] Calculate hit windows in performance calculator
instead of databased difficulty attributes (#31735)
* Calculate hit windows in performance calculator instead of databased difficulty attributes
* Apply mods to beatmap difficulty in osu! performance calculator
* Remove `GreatHitWindow` difficulty attribute for osu!mania
* Remove use of approach rate and overall difficulty attributes for osu!
* Remove use of hit window difficulty attributes in osu!taiko
* Remove use of approach rate attribute in osu!catch
* Remove unused attribute IDs
* Code quality
* Fix `computeDeviationUpperBound` being called before `greatHitWindow` is set
---
.../Difficulty/CatchDifficultyAttributes.cs | 12 ---
.../Difficulty/CatchDifficultyCalculator.cs | 4 -
.../Difficulty/CatchPerformanceCalculator.cs | 17 ++++-
.../Difficulty/ManiaDifficultyAttributes.cs | 12 ---
.../Difficulty/ManiaDifficultyCalculator.cs | 29 --------
.../Difficulty/OsuDifficultyAttributes.cs | 41 ----------
.../Difficulty/OsuDifficultyCalculator.cs | 15 ----
.../Difficulty/OsuPerformanceCalculator.cs | 74 +++++++++++++------
.../Difficulty/TaikoDifficultyAttributes.cs | 22 ------
.../Difficulty/TaikoDifficultyCalculator.cs | 5 --
.../Difficulty/TaikoPerformanceCalculator.cs | 30 ++++++--
.../Difficulty/DifficultyAttributes.cs | 6 --
12 files changed, 92 insertions(+), 175 deletions(-)
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
index 5c64643fd4..82c3cfe735 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
-using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
@@ -10,15 +9,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyAttributes : DifficultyAttributes
{
- ///
- /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
- ///
- ///
- /// 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; }
-
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
{
foreach (var v in base.ToDatabaseAttributes())
@@ -26,7 +16,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
// Todo: osu!catch should not output star rating in the 'aim' attribute.
yield return (ATTRIB_ID_AIM, StarRating);
- yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
}
public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo)
@@ -34,7 +23,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
base.FromDatabaseAttributes(values, onlineInfo);
StarRating = values[ATTRIB_ID_AIM];
- ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index 99df2731ff..6434adb63c 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -36,14 +36,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
if (beatmap.HitObjects.Count == 0)
return new CatchDifficultyAttributes { Mods = mods };
- // this is the same as osu!, so there's potential to share the implementation... maybe
- double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
-
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
{
StarRating = Math.Sqrt(skills.OfType().Single().DifficultyValue()) * difficulty_multiplier,
Mods = mods,
- ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
MaxCombo = beatmap.GetMaxCombo(),
};
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index 55232a9598..62a9fe250e 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -3,6 +3,9 @@
using System;
using System.Linq;
+using osu.Framework.Audio.Track;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
@@ -50,7 +53,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty
if (catchAttributes.MaxCombo > 0)
value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
- double approachRate = catchAttributes.ApproachRate;
+ var difficulty = score.BeatmapInfo!.Difficulty.Clone();
+
+ score.Mods.OfType().ForEach(m => m.ApplyToDifficulty(difficulty));
+
+ var track = new TrackVirtual(10000);
+ score.Mods.OfType().ForEach(m => m.ApplyToTrack(track));
+ double clockRate = track.Rate;
+
+ // this is the same as osu!, so there's potential to share the implementation... maybe
+ double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
+
+ double approachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0;
+
double approachRateFactor = 1.0;
if (approachRate > 9.0)
approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
index db60e757e1..512d98f713 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
-using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
@@ -10,22 +9,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
public class ManiaDifficultyAttributes : DifficultyAttributes
{
- ///
- /// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
- ///
- ///
- /// Rate-adjusting mods do not affect the hit window at all in osu-stable.
- ///
- [JsonProperty("great_hit_window")]
- public double GreatHitWindow { get; set; }
-
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
{
foreach (var v in base.ToDatabaseAttributes())
yield return v;
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
- yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
}
public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo)
@@ -33,7 +22,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
base.FromDatabaseAttributes(values, onlineInfo);
StarRating = values[ATTRIB_ID_DIFFICULTY];
- GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index 1efa7cb42f..06b8018f2b 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private const double difficulty_multiplier = 0.018;
private readonly bool isForCurrentRuleset;
- private readonly double originalOverallDifficulty;
public override int Version => 20241007;
@@ -35,7 +34,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
: base(ruleset, beatmap)
{
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
- originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
@@ -50,9 +48,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
StarRating = skills.OfType().Single().DifficultyValue() * difficulty_multiplier,
Mods = mods,
- // In osu-stable mania, rate-adjustment mods don't affect the hit window.
- // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
- GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject),
};
@@ -124,29 +119,5 @@ namespace osu.Game.Rulesets.Mania.Difficulty
}).ToArray();
}
}
-
- private double getHitWindow300(Mod[] mods)
- {
- if (isForCurrentRuleset)
- {
- double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty));
- return applyModAdjustments(34 + 3 * od, mods);
- }
-
- if (Math.Round(originalOverallDifficulty) > 4)
- return applyModAdjustments(34, mods);
-
- return applyModAdjustments(47, mods);
-
- static double applyModAdjustments(double value, Mod[] mods)
- {
- if (mods.Any(m => m is ManiaModHardRock))
- value /= 1.4;
- else if (mods.Any(m => m is ManiaModEasy))
- value *= 1.4;
-
- return value;
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
index 395f581b65..f7d8c649c1 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
@@ -59,36 +59,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("speed_difficult_strain_count")]
public double SpeedDifficultStrainCount { get; set; }
- ///
- /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
- ///
- [JsonProperty("approach_rate")]
- public double ApproachRate { get; set; }
-
- ///
- /// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc).
- ///
- [JsonProperty("overall_difficulty")]
- public double OverallDifficulty { get; set; }
-
- ///
- /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
- ///
- [JsonProperty("great_hit_window")]
- public double GreatHitWindow { get; set; }
-
- ///
- /// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc).
- ///
- [JsonProperty("ok_hit_window")]
- public double OkHitWindow { get; set; }
-
- ///
- /// The perceived hit window for a MEH hit inclusive of rate-adjusting mods (DT/HT/etc).
- ///
- [JsonProperty("meh_hit_window")]
- public double MehHitWindow { get; set; }
-
///
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
///
@@ -116,10 +86,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_AIM, AimDifficulty);
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
- yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
- yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
- yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
if (ShouldSerializeFlashlightDifficulty())
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
@@ -130,9 +97,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
-
- yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
- yield return (ATTRIB_ID_MEH_HIT_WINDOW, MehHitWindow);
}
public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo)
@@ -141,18 +105,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
AimDifficulty = values[ATTRIB_ID_AIM];
SpeedDifficulty = values[ATTRIB_ID_SPEED];
- OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
- ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
StarRating = values[ATTRIB_ID_DIFFICULTY];
- GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
- OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
- MehHitWindow = values[ATTRIB_ID_MEH_HIT_WINDOW];
DrainRate = onlineInfo.DrainRate;
HitCircleCount = onlineInfo.CircleCount;
SliderCount = onlineInfo.SliderCount;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 1505c51592..30339fbaa7 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -15,8 +15,6 @@ using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Osu.Scoring;
-using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Difficulty
{
@@ -90,20 +88,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4)
: 0;
- double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate;
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
- HitWindows hitWindows = new OsuHitWindows();
- hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
-
- double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
- double hitWindowOk = hitWindows.WindowFor(HitResult.Ok) / clockRate;
- double hitWindowMeh = hitWindows.WindowFor(HitResult.Meh) / clockRate;
-
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
{
StarRating = starRating,
@@ -116,11 +106,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
SliderFactor = sliderFactor,
AimDifficultStrainCount = aimDifficultyStrainCount,
SpeedDifficultStrainCount = speedDifficultyStrainCount,
- ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
- OverallDifficulty = (80 - hitWindowGreat) / 6,
- GreatHitWindow = hitWindowGreat,
- OkHitWindow = hitWindowOk,
- MehHitWindow = hitWindowMeh,
DrainRate = drainRate,
MaxCombo = beatmap.GetMaxCombo(),
HitCircleCount = hitCirclesCount,
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index dc2df39cdb..09ec890926 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -4,10 +4,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Audio.Track;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@@ -41,6 +46,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
///
private double effectiveMissCount;
+ private double clockRate;
+ private double greatHitWindow;
+ private double okHitWindow;
+ private double mehHitWindow;
+ private double overallDifficulty;
+ private double approachRate;
+
private double? speedDeviation;
public OsuPerformanceCalculator()
@@ -64,6 +76,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss);
effectiveMissCount = countMiss;
+ var difficulty = score.BeatmapInfo!.Difficulty.Clone();
+
+ score.Mods.OfType().ForEach(m => m.ApplyToDifficulty(difficulty));
+
+ var track = new TrackVirtual(10000);
+ score.Mods.OfType().ForEach(m => m.ApplyToTrack(track));
+ clockRate = track.Rate;
+
+ HitWindows hitWindows = new OsuHitWindows();
+ hitWindows.SetDifficulty(difficulty.OverallDifficulty);
+
+ greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate;
+ okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate;
+ mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate;
+
+ double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
+
+ overallDifficulty = (80 - greatHitWindow) / 6;
+ approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5;
+
if (osuAttributes.SliderCount > 0)
{
if (usingClassicSliderAccuracy)
@@ -106,8 +138,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// https://www.desmos.com/calculator/bc9eybdthb
// we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
// this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
- double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0);
- double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0);
+ double okMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 1.8) : 1.0);
+ double mehMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 5) : 1.0);
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
@@ -178,10 +210,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount);
double approachRateFactor = 0.0;
- if (attributes.ApproachRate > 10.33)
- approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
- else if (attributes.ApproachRate < 8.0)
- approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate);
+ if (approachRate > 10.33)
+ approachRateFactor = 0.3 * (approachRate - 10.33);
+ else if (approachRate < 8.0)
+ approachRateFactor = 0.05 * (8.0 - approachRate);
if (score.Mods.Any(h => h is OsuModRelax))
approachRateFactor = 0.0;
@@ -193,12 +225,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
- aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
+ aimValue *= 1.0 + 0.04 * (12.0 - approachRate);
}
aimValue *= accuracy;
// It is important to consider accuracy difficulty when scaling with accuracy.
- aimValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500;
+ aimValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
return aimValue;
}
@@ -218,8 +250,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount);
double approachRateFactor = 0.0;
- if (attributes.ApproachRate > 10.33)
- approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
+ if (approachRate > 10.33)
+ approachRateFactor = 0.3 * (approachRate - 10.33);
if (score.Mods.Any(h => h is OsuModAutopilot))
approachRateFactor = 0.0;
@@ -234,7 +266,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
- speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
+ speedValue *= 1.0 + 0.04 * (12.0 - approachRate);
}
double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
@@ -248,7 +280,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
// Scale the speed value with accuracy and OD.
- speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
+ speedValue *= (0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - overallDifficulty) / 2);
return speedValue;
}
@@ -275,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.
- double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
+ double accuracyValue = Math.Pow(1.52163, overallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
@@ -312,7 +344,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Scale the flashlight value with accuracy _slightly_.
flashlightValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that.
- flashlightValue *= 0.98 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 2500;
+ flashlightValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
return flashlightValue;
}
@@ -352,10 +384,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss;
- double hitWindowGreat = attributes.GreatHitWindow;
- double hitWindowOk = attributes.OkHitWindow;
- double hitWindowMeh = attributes.MehHitWindow;
-
// The probability that a player hits a circle is unknown, but we can estimate it to be
// the number of greats on circles divided by the number of circles, and then add one
// to the number of circles as a bias correction.
@@ -370,22 +398,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed.
// Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than:
- double deviation = hitWindowGreat / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound));
+ double deviation = greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound));
- double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2))
- / (deviation * DifficultyCalculationUtils.Erf(hitWindowOk / (Math.Sqrt(2) * deviation)));
+ double randomValue = Math.Sqrt(2 / Math.PI) * okHitWindow * Math.Exp(-0.5 * Math.Pow(okHitWindow / deviation, 2))
+ / (deviation * DifficultyCalculationUtils.Erf(okHitWindow / (Math.Sqrt(2) * deviation)));
deviation *= Math.Sqrt(1 - randomValue);
// Value deviation approach as greatCount approaches 0
- double limitValue = hitWindowOk / Math.Sqrt(3);
+ double limitValue = okHitWindow / Math.Sqrt(3);
// If precision is not enough to compute true deviation - use limit value
if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue)
deviation = limitValue;
// Then compute the variance for mehs.
- double mehVariance = (hitWindowMeh * hitWindowMeh + hitWindowOk * hitWindowMeh + hitWindowOk * hitWindowOk) / 3;
+ double mehVariance = (mehHitWindow * mehHitWindow + okHitWindow * mehHitWindow + okHitWindow * okHitWindow) / 3;
// Find the total deviation.
deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh));
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
index 37e6996e5a..b43468ab18 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
@@ -49,32 +49,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
[JsonProperty("stamina_difficult_strains")]
public double StaminaTopStrains { get; set; }
- ///
- /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
- ///
- ///
- /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing.
- ///
- [JsonProperty("great_hit_window")]
- public double GreatHitWindow { get; set; }
-
- ///
- /// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc).
- ///
- ///
- /// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing.
- ///
- [JsonProperty("ok_hit_window")]
- public double OkHitWindow { get; set; }
-
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
{
foreach (var v in base.ToDatabaseAttributes())
yield return v;
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
- yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
- yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
yield return (ATTRIB_ID_MONO_STAMINA_FACTOR, MonoStaminaFactor);
}
@@ -83,8 +63,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
base.FromDatabaseAttributes(values, onlineInfo);
StarRating = values[ATTRIB_ID_DIFFICULTY];
- GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
- OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
MonoStaminaFactor = values[ATTRIB_ID_MONO_STAMINA_FACTOR];
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index 6b9986bd68..7bc050d2df 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -129,9 +129,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert);
double starRating = rescale(combinedRating * 1.4);
- HitWindows hitWindows = new TaikoHitWindows();
- hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
-
TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes
{
StarRating = starRating,
@@ -144,8 +141,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
RhythmTopStrains = rhythmDifficultStrains,
ColourTopStrains = colourDifficultStrains,
StaminaTopStrains = staminaDifficultStrains,
- GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
- OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
MaxCombo = beatmap.GetMaxCombo(),
};
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index bcd3693119..9e049df87c 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -4,11 +4,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Audio.Track;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Taiko.Difficulty
@@ -21,6 +24,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private int countMiss;
private double? estimatedUnstableRate;
+ private double clockRate;
+ private double greatHitWindow;
+
private double effectiveMissCount;
public TaikoPerformanceCalculator()
@@ -36,7 +42,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
- estimatedUnstableRate = computeDeviationUpperBound(taikoAttributes) * 10;
+
+ var track = new TrackVirtual(10000);
+ score.Mods.OfType().ForEach(m => m.ApplyToTrack(track));
+ clockRate = track.Rate;
+
+ var difficulty = score.BeatmapInfo!.Difficulty.Clone();
+
+ score.Mods.OfType().ForEach(m => m.ApplyToDifficulty(difficulty));
+
+ HitWindows hitWindows = new TaikoHitWindows();
+ hitWindows.SetDifficulty(difficulty.OverallDifficulty);
+
+ greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate;
+
+ estimatedUnstableRate = computeDeviationUpperBound() * 10;
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
if (totalSuccessfulHits > 0)
@@ -104,7 +124,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
{
- if (attributes.GreatHitWindow <= 0 || estimatedUnstableRate == null)
+ if (greatHitWindow <= 0 || estimatedUnstableRate == null)
return 0;
double accuracyValue = Math.Pow(70 / estimatedUnstableRate.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0;
@@ -123,9 +143,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
/// and the hit judgements, assuming the player's mean hit error is 0. The estimation is consistent in that
/// two SS scores on the same map with the same settings will always return the same deviation.
///
- private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes)
+ private double? computeDeviationUpperBound()
{
- if (countGreat == 0 || attributes.GreatHitWindow <= 0)
+ if (countGreat == 0 || greatHitWindow <= 0)
return null;
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
@@ -139,7 +159,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
// We can be 99% confident that the deviation is not higher than:
- return attributes.GreatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound));
+ return greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound));
}
private int totalHits => countGreat + countOk + countMeh + countMiss;
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
index 1d6cee043b..59511973f7 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
@@ -17,21 +17,15 @@ namespace osu.Game.Rulesets.Difficulty
{
protected const int ATTRIB_ID_AIM = 1;
protected const int ATTRIB_ID_SPEED = 3;
- protected const int ATTRIB_ID_OVERALL_DIFFICULTY = 5;
- protected const int ATTRIB_ID_APPROACH_RATE = 7;
protected const int ATTRIB_ID_MAX_COMBO = 9;
protected const int ATTRIB_ID_DIFFICULTY = 11;
- protected const int ATTRIB_ID_GREAT_HIT_WINDOW = 13;
- protected const int ATTRIB_ID_SCORE_MULTIPLIER = 15;
protected const int ATTRIB_ID_FLASHLIGHT = 17;
protected const int ATTRIB_ID_SLIDER_FACTOR = 19;
protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21;
protected const int ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT = 23;
protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 25;
- protected const int ATTRIB_ID_OK_HIT_WINDOW = 27;
protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29;
protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31;
- protected const int ATTRIB_ID_MEH_HIT_WINDOW = 33;
///
/// The mods which were applied to the beatmap.
From d4ce71267256590ff170281b17ef471fdb497653 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Mon, 10 Feb 2025 17:57:01 +0900
Subject: [PATCH 076/262] Add note about weird taiko iteration
---
.../Difficulty/Utils/IntervalGroupingUtils.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
index 7bd7aa7677..5ab58ad4f3 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Utils/IntervalGroupingUtils.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Utils
{
const double margin_of_error = 5;
+ // This never compares the first two elements in the group.
+ // This sounds wrong but is apparently "as intended" (https://github.com/ppy/osu/pull/31636#discussion_r1942673329)
var groupedObjects = new List { objects[i] };
i++;
From 340e081965355fc1f36acdd1acce1d7c6b773780 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Mon, 10 Feb 2025 18:05:08 +0900
Subject: [PATCH 077/262] Rename buzz variable per review
---
osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index 2d1adbd056..559e9dafa0 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
private float lastDistanceMoved;
private float lastExactDistanceMoved;
private double lastStrainTime;
- private bool isBuzzSliderTriggered;
+ private bool isInBuzzSection;
///
/// The speed multiplier applied to the player's catcher.
@@ -107,14 +107,14 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius)
if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime)
{
- if (isBuzzSliderTriggered)
+ if (isInBuzzSection)
distanceAddition = 0;
else
- isBuzzSliderTriggered = true;
+ isInBuzzSection = true;
}
else
{
- isBuzzSliderTriggered = false;
+ isInBuzzSection = false;
}
lastPlayerPosition = playerPosition;
From 8c85616d1c8677a859bf007291997b092786f94c Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Tue, 11 Feb 2025 21:28:21 +0900
Subject: [PATCH 078/262] Fix test
---
osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs
index 66c465cbed..bd1e15d06d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Bindable playingState = new Bindable();
GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState);
TestSpectatorClient spectatorClient = new TestSpectatorClient();
- TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestMultiplayerRoomManager(new TestRoomRequestsHandler()));
+ TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestRoomRequestsHandler());
AddStep("create spectator list", () =>
{
From 068a66e7d4c9bef92ea39e5237b37bc628e9e14f Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 12 Feb 2025 18:35:35 +0900
Subject: [PATCH 079/262] Move room tracking to lounge subscreen
---
.../TestSceneLoungeRoomsContainer.cs | 28 ++++-----
.../TestScenePlaylistsLoungeSubScreen.cs | 30 ++++-----
.../Components/ListingPollingComponent.cs | 38 ++----------
.../Components/SelectionPollingComponent.cs | 5 +-
.../Lounge/Components/RoomsContainer.cs | 45 ++++----------
.../OnlinePlay/Lounge/LoungeSubScreen.cs | 62 ++++++++++++++-----
.../Multiplayer/MultiplayerLoungeSubScreen.cs | 34 ----------
.../Playlists/PlaylistsLoungeSubScreen.cs | 3 -
8 files changed, 95 insertions(+), 150 deletions(-)
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
index 797b69ec72..10df77f88c 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
@@ -50,17 +50,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("add rooms", () => RoomManager.AddRooms(5, withSpotlightRooms: true));
- AddAssert("has 5 rooms", () => container.Rooms.Count == 5);
+ AddAssert("has 5 rooms", () => container.DrawableRooms.Count == 5);
- AddAssert("all spotlights at top", () => container.Rooms
+ AddAssert("all spotlights at top", () => container.DrawableRooms
.SkipWhile(r => r.Room.Category == RoomCategory.Spotlight)
.All(r => r.Room.Category == RoomCategory.Normal));
AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.First(r => r.RoomID == 0)));
- AddAssert("has 4 rooms", () => container.Rooms.Count == 4);
- AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID != 0));
+ AddAssert("has 4 rooms", () => container.DrawableRooms.Count == 4);
+ AddAssert("first room removed", () => container.DrawableRooms.All(r => r.Room.RoomID != 0));
- AddStep("select first room", () => container.Rooms.First().TriggerClick());
+ AddStep("select first room", () => container.DrawableRooms.First().TriggerClick());
AddAssert("first spotlight selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category == RoomCategory.Spotlight)));
AddStep("remove last room", () => RoomManager.RemoveRoom(RoomManager.Rooms.MinBy(r => r.RoomID)!));
@@ -137,15 +137,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("add rooms", () => RoomManager.AddRooms(4));
- AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
+ AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4);
AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = "1" });
- AddUntilStep("1 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 1);
+ AddUntilStep("1 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 1);
AddStep("remove filter", () => container.Filter.Value = null);
- AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
+ AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4);
}
[Test]
@@ -156,13 +156,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
// Todo: What even is this case...?
AddStep("set empty filter criteria", () => container.Filter.Value = new FilterCriteria());
- AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5);
+ AddUntilStep("5 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 5);
AddStep("filter osu! rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo });
- AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2);
+ AddUntilStep("2 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 2);
AddStep("filter catch rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo });
- AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3);
+ AddUntilStep("3 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 3);
}
[Test]
@@ -176,15 +176,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("apply default filter", () => container.Filter.SetDefault());
- AddUntilStep("both rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2);
+ AddUntilStep("both rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 2);
AddStep("filter public rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Public });
- AddUntilStep("private room hidden", () => container.Rooms.All(r => !r.Room.HasPassword));
+ AddUntilStep("private room hidden", () => container.DrawableRooms.All(r => !r.Room.HasPassword));
AddStep("filter private rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Private });
- AddUntilStep("public room hidden", () => container.Rooms.All(r => r.Room.HasPassword));
+ AddUntilStep("public room hidden", () => container.DrawableRooms.All(r => r.Room.HasPassword));
}
[Test]
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
index 53c7873de5..9d65be2a19 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Playlists
public void TestManyRooms()
{
AddStep("add rooms", () => RoomManager.AddRooms(500));
- AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 500);
+ AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 500);
}
[Test]
@@ -45,45 +45,45 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddStep("add rooms", () => RoomManager.AddRooms(30));
- AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30);
+ AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30);
- AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0]));
+ AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0]));
- AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.Rooms[2]));
+ AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[2]));
AddStep("hold down", () => InputManager.PressButton(MouseButton.Left));
- AddStep("drag to top", () => InputManager.MoveMouseTo(roomsContainer.Rooms[0]));
+ AddStep("drag to top", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[0]));
AddAssert("first and second room masked", ()
- => !checkRoomVisible(roomsContainer.Rooms[0]) &&
- !checkRoomVisible(roomsContainer.Rooms[1]));
+ => !checkRoomVisible(roomsContainer.DrawableRooms[0]) &&
+ !checkRoomVisible(roomsContainer.DrawableRooms[1]));
}
[Test]
public void TestScrollSelectedIntoView()
{
AddStep("add rooms", () => RoomManager.AddRooms(30));
- AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30);
+ AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30);
- AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0]));
+ AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0]));
- AddStep("select last room", () => roomsContainer.Rooms[^1].TriggerClick());
+ AddStep("select last room", () => roomsContainer.DrawableRooms[^1].TriggerClick());
- AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.Rooms[0]));
- AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms[^1]));
+ AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.DrawableRooms[0]));
+ AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[^1]));
}
[Test]
public void TestEnteringRoomTakesLeaseOnSelection()
{
AddStep("add rooms", () => RoomManager.AddRooms(1));
- AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 1);
+ AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 1);
AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled);
- AddStep("select room", () => roomsContainer.Rooms[0].TriggerClick());
+ AddStep("select room", () => roomsContainer.DrawableRooms[0].TriggerClick());
AddAssert("selected room is non-null", () => loungeScreen.SelectedRoom.Value != null);
- AddStep("enter room", () => roomsContainer.Rooms[0].TriggerClick());
+ AddStep("enter room", () => roomsContainer.DrawableRooms[0].TriggerClick());
AddUntilStep("wait for match load", () => Stack.CurrentScreen is PlaylistsRoomSubScreen);
diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs
index 21452727b8..5cb4c9420a 100644
--- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs
@@ -1,9 +1,9 @@
// 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 System.Threading.Tasks;
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
@@ -15,23 +15,8 @@ namespace osu.Game.Screens.OnlinePlay.Components
///
public partial class ListingPollingComponent : RoomPollingComponent
{
- public IBindable InitialRoomsReceived => initialRoomsReceived;
- private readonly Bindable initialRoomsReceived = new Bindable();
-
- public readonly Bindable Filter = new Bindable();
-
- [BackgroundDependencyLoader]
- private void load()
- {
- Filter.BindValueChanged(_ =>
- {
- RoomManager.ClearRooms();
- initialRoomsReceived.Value = false;
-
- if (IsLoaded)
- PollImmediately();
- });
- }
+ public required Action RoomsReceived { get; init; }
+ public readonly IBindable Filter = new Bindable();
private GetRoomsRequest? lastPollRequest;
@@ -43,26 +28,14 @@ namespace osu.Game.Screens.OnlinePlay.Components
if (Filter.Value == null)
return base.Poll();
- var tcs = new TaskCompletionSource();
-
lastPollRequest?.Cancel();
+ var tcs = new TaskCompletionSource();
var req = new GetRoomsRequest(Filter.Value);
req.Success += result =>
{
- result = result.Where(r => r.Category != RoomCategory.DailyChallenge).ToList();
-
- foreach (var existing in RoomManager.Rooms.ToArray())
- {
- if (result.All(r => r.RoomID != existing.RoomID))
- RoomManager.RemoveRoom(existing);
- }
-
- foreach (var incoming in result)
- RoomManager.AddOrUpdateRoom(incoming);
-
- initialRoomsReceived.Value = true;
+ RoomsReceived(result.Where(r => r.Category != RoomCategory.DailyChallenge).ToArray());
tcs.SetResult(true);
};
@@ -71,6 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
API.Queue(req);
lastPollRequest = req;
+
return tcs.Task;
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs
index 7cee8b3546..f04fd6a096 100644
--- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs
@@ -28,15 +28,14 @@ namespace osu.Game.Screens.OnlinePlay.Components
if (room.RoomID == null)
return base.Poll();
- var tcs = new TaskCompletionSource();
-
lastPollRequest?.Cancel();
+ var tcs = new TaskCompletionSource();
var req = new GetRoomRequest(room.RoomID.Value);
req.Success += result =>
{
- RoomManager.AddOrUpdateRoom(result);
+ room.CopyFrom(result);
tcs.SetResult(true);
};
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
index 6eda993f94..6681cbe720 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
@@ -7,10 +7,8 @@ using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
-using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
@@ -24,17 +22,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public partial class RoomsContainer : CompositeDrawable, IKeyBindingHandler
{
+ public readonly BindableList Rooms = new BindableList();
public readonly Bindable SelectedRoom = new Bindable();
public readonly Bindable Filter = new Bindable();
- public IReadOnlyList Rooms => roomFlow.FlowingChildren.Cast().ToArray();
+ public IReadOnlyList DrawableRooms => roomFlow.FlowingChildren.Cast().ToArray();
- private readonly IBindableList rooms = new BindableList();
private readonly FillFlowContainer roomFlow;
- [Resolved]
- private IRoomManager roomManager { get; set; } = null!;
-
// handle deselection
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
@@ -62,11 +57,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
protected override void LoadComplete()
{
- rooms.CollectionChanged += roomsChanged;
- roomManager.RoomsUpdated += updateSorting;
-
- rooms.BindTo(roomManager.Rooms);
-
+ Rooms.BindCollectionChanged(roomsChanged, true);
Filter.BindValueChanged(criteria => applyFilterCriteria(criteria.NewValue), true);
}
@@ -155,7 +146,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
private void addRooms(IEnumerable rooms)
{
foreach (var room in rooms)
- roomFlow.Add(new DrawableLoungeRoom(room) { SelectedRoom = SelectedRoom });
+ {
+ var drawableRoom = new DrawableLoungeRoom(room) { SelectedRoom = SelectedRoom };
+
+ roomFlow.Add(drawableRoom);
+
+ // Always show spotlight playlists at the top of the listing.
+ roomFlow.SetLayoutPosition(drawableRoom, room.Category > RoomCategory.Normal ? float.MinValue : -(room.RoomID ?? 0));
+ }
applyFilterCriteria(Filter.Value);
}
@@ -181,17 +179,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
SelectedRoom.Value = null;
}
- private void updateSorting()
- {
- foreach (var room in roomFlow)
- {
- roomFlow.SetLayoutPosition(room, room.Room.Category > RoomCategory.Normal
- // Always show spotlight playlists at the top of the listing.
- ? float.MinValue
- : -(room.Room.RoomID ?? 0));
- }
- }
-
protected override bool OnClick(ClickEvent e)
{
if (!SelectedRoom.Disabled)
@@ -226,7 +213,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
if (SelectedRoom.Disabled)
return;
- var visibleRooms = Rooms.AsEnumerable().Where(r => r.IsPresent);
+ var visibleRooms = DrawableRooms.AsEnumerable().Where(r => r.IsPresent);
Room? room;
@@ -246,13 +233,5 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
}
#endregion
-
- protected override void Dispose(bool isDisposing)
- {
- base.Dispose(isDisposing);
-
- if (roomManager.IsNotNull())
- roomManager.RoomsUpdated -= updateSorting;
- }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
index 0e08e398a4..78501a56d7 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
@@ -53,8 +53,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
AutoSizeAxes = Axes.Both
};
- protected ListingPollingComponent ListingPollingComponent { get; private set; } = null!;
-
protected readonly Bindable SelectedRoom = new Bindable();
[Resolved]
@@ -75,12 +73,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
[Resolved]
protected OsuConfigManager Config { get; private set; } = null!;
- private IDisposable? joiningRoomOperation { get; set; }
+ private IDisposable? joiningRoomOperation;
private LeasedBindable? selectionLease;
+ private readonly BindableList rooms = new BindableList();
private readonly Bindable filter = new Bindable();
+ private readonly Bindable hasListingResults = new Bindable();
private readonly IBindable operationInProgress = new Bindable();
private readonly IBindable isIdle = new BindableBool();
+ private ListingPollingComponent listingPollingComponent = null!;
private PopoverContainer popoverContainer = null!;
private LoadingLayer loadingLayer = null!;
private RoomsContainer roomsContainer = null!;
@@ -100,7 +101,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
InternalChildren = new Drawable[]
{
- ListingPollingComponent = CreatePollingComponent().With(c => c.Filter.BindTarget = filter),
+ listingPollingComponent = new ListingPollingComponent
+ {
+ RoomsReceived = onListingReceived,
+ Filter = { BindTarget = filter }
+ },
popoverContainer = new PopoverContainer
{
Name = @"Rooms area",
@@ -116,8 +121,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
ScrollbarOverlapsContent = false,
Child = roomsContainer = new RoomsContainer
{
+ Rooms = { BindTarget = rooms },
+ SelectedRoom = { BindTarget = SelectedRoom },
Filter = { BindTarget = filter },
- SelectedRoom = { BindTarget = SelectedRoom }
}
},
},
@@ -178,7 +184,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
// scroll selected room into view on selection.
SelectedRoom.BindValueChanged(val =>
{
- var drawable = roomsContainer.Rooms.FirstOrDefault(r => r.Room == val.NewValue);
+ var drawable = roomsContainer.DrawableRooms.FirstOrDefault(r => r.Room == val.NewValue);
if (drawable != null)
scrollContainer.ScrollIntoView(drawable);
});
@@ -190,7 +196,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
searchTextBox.Current.BindValueChanged(_ => updateFilterDebounced());
ruleset.BindValueChanged(_ => UpdateFilter());
-
isIdle.BindValueChanged(_ => updatePollingRate(this.IsCurrentScreen()), true);
if (ongoingOperationTracker != null)
@@ -199,11 +204,38 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
operationInProgress.BindValueChanged(_ => updateLoadingLayer());
}
- ListingPollingComponent.InitialRoomsReceived.BindValueChanged(_ => updateLoadingLayer(), true);
+ hasListingResults.BindValueChanged(_ => updateLoadingLayer());
+
+ filter.BindValueChanged(_ =>
+ {
+ rooms.Clear();
+ hasListingResults.Value = false;
+ listingPollingComponent.PollImmediately();
+ });
updateFilter();
}
+ private void onListingReceived(Room[] result)
+ {
+ Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value);
+ Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value);
+
+ // Remove all local rooms no longer in the result set.
+ rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value));
+
+ // Add or update local rooms with the result set.
+ foreach (var r in result)
+ {
+ if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom))
+ existingRoom.CopyFrom(r);
+ else
+ rooms.Add(r);
+ }
+
+ hasListingResults.Value = true;
+ }
+
#region Filtering
public void UpdateFilter() => Scheduler.AddOnce(updateFilter);
@@ -267,7 +299,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
onReturning();
// Poll for any newly-created rooms (including potentially the user's own).
- ListingPollingComponent.PollImmediately();
+ listingPollingComponent.PollImmediately();
}
public override bool OnExiting(ScreenExitEvent e)
@@ -392,11 +424,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
this.Push(CreateRoomSubScreen(room));
}
- public void RefreshRooms() => ListingPollingComponent.PollImmediately();
+ public void RefreshRooms() => listingPollingComponent.PollImmediately();
private void updateLoadingLayer()
{
- if (operationInProgress.Value || !ListingPollingComponent.InitialRoomsReceived.Value)
+ if (operationInProgress.Value || !hasListingResults.Value)
loadingLayer.Show();
else
loadingLayer.Hide();
@@ -405,11 +437,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private void updatePollingRate(bool isCurrentScreen)
{
if (!isCurrentScreen)
- ListingPollingComponent.TimeBetweenPolls.Value = 0;
+ listingPollingComponent.TimeBetweenPolls.Value = 0;
else
- ListingPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000;
+ listingPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000;
- Logger.Log($"Polling adjusted (listing: {ListingPollingComponent.TimeBetweenPolls.Value})");
+ Logger.Log($"Polling adjusted (listing: {listingPollingComponent.TimeBetweenPolls.Value})");
}
protected abstract OsuButton CreateNewRoomButton();
@@ -421,7 +453,5 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
protected abstract Room CreateNewRoom();
protected abstract RoomSubScreen CreateRoomSubScreen(Room room);
-
- protected abstract ListingPollingComponent CreatePollingComponent();
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
index 873a9cde88..3cf873ec78 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
@@ -79,8 +79,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room);
- protected override ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent();
-
protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure)
{
client.JoinRoom(room, password).ContinueWith(result =>
@@ -109,37 +107,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
base.OpenNewRoom(room);
}
-
- private partial class MultiplayerListingPollingComponent : ListingPollingComponent
- {
- [Resolved]
- private MultiplayerClient client { get; set; } = null!;
-
- private readonly IBindable isConnected = new Bindable();
-
- [BackgroundDependencyLoader]
- private void load()
- {
- isConnected.BindTo(client.IsConnected);
- isConnected.BindValueChanged(_ => Scheduler.AddOnce(poll), true);
- }
-
- private void poll()
- {
- if (isConnected.Value && IsLoaded)
- PollImmediately();
- }
-
- protected override Task Poll()
- {
- if (!isConnected.Value)
- return Task.CompletedTask;
-
- if (client.Room != null)
- return Task.CompletedTask;
-
- return base.Poll();
- }
- }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
index 6ed367328c..26eae50797 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
@@ -11,7 +11,6 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
-using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
@@ -87,8 +86,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room);
- protected override ListingPollingComponent CreatePollingComponent() => new ListingPollingComponent();
-
private enum PlaylistsCategory
{
Any,
From f146a7d116bbebec4880c8a3dd7124d20dc58022 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 12 Feb 2025 21:09:58 +0900
Subject: [PATCH 080/262] Remove `RoomManager` and related components
---
.../TestSceneLoungeRoomsContainer.cs | 54 ++++++------
.../TestSceneMultiplayerLoungeSubScreen.cs | 33 +++++---
.../TestScenePlaylistsLoungeSubScreen.cs | 30 +++----
.../TestScenePlaylistsMatchSettingsOverlay.cs | 2 -
.../Components/ListingPollingComponent.cs | 14 +++-
.../OnlinePlay/Components/RoomManager.cs | 82 -------------------
.../Components/RoomPollingComponent.cs | 18 ----
.../Components/SelectionPollingComponent.cs | 14 +++-
osu.Game/Screens/OnlinePlay/IRoomManager.cs | 42 ----------
.../OnlinePlay/Lounge/LoungeSubScreen.cs | 12 +--
.../Screens/OnlinePlay/OnlinePlayScreen.cs | 5 --
.../IOnlinePlayTestSceneDependencies.cs | 5 --
.../Visual/OnlinePlay/OnlinePlayTestScene.cs | 32 +++++++-
.../OnlinePlayTestSceneDependencies.cs | 3 -
.../Visual/OnlinePlay/TestRoomManager.cs | 59 -------------
.../OnlinePlay/TestRoomRequestsHandler.cs | 3 +-
16 files changed, 121 insertions(+), 287 deletions(-)
delete mode 100644 osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
delete mode 100644 osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs
delete mode 100644 osu.Game/Screens/OnlinePlay/IRoomManager.cs
delete mode 100644 osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
index 10df77f88c..9daad960c7 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
@@ -3,6 +3,7 @@
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@@ -19,8 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene
{
- protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
-
+ private BindableList rooms = null!;
private RoomsContainer container = null!;
public override void SetUpSteps()
@@ -29,6 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create container", () =>
{
+ rooms = new BindableList();
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.X,
@@ -36,9 +37,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
-
Child = container = new RoomsContainer
{
+ Rooms = { BindTarget = rooms },
SelectedRoom = { BindTarget = SelectedRoom }
}
};
@@ -48,7 +49,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestBasicListChanges()
{
- AddStep("add rooms", () => RoomManager.AddRooms(5, withSpotlightRooms: true));
+ AddStep("add rooms", () => rooms.AddRange(GenerateRooms(5, withSpotlightRooms: true)));
AddAssert("has 5 rooms", () => container.DrawableRooms.Count == 5);
@@ -56,49 +57,50 @@ namespace osu.Game.Tests.Visual.Multiplayer
.SkipWhile(r => r.Room.Category == RoomCategory.Spotlight)
.All(r => r.Room.Category == RoomCategory.Normal));
- AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.First(r => r.RoomID == 0)));
+ AddStep("remove first room", () => rooms.RemoveAt(0));
AddAssert("has 4 rooms", () => container.DrawableRooms.Count == 4);
AddAssert("first room removed", () => container.DrawableRooms.All(r => r.Room.RoomID != 0));
AddStep("select first room", () => container.DrawableRooms.First().TriggerClick());
- AddAssert("first spotlight selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category == RoomCategory.Spotlight)));
+ AddAssert("first spotlight selected", () => checkRoomSelected(rooms.First(r => r.Category == RoomCategory.Spotlight)));
- AddStep("remove last room", () => RoomManager.RemoveRoom(RoomManager.Rooms.MinBy(r => r.RoomID)!));
- AddAssert("first spotlight still selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category == RoomCategory.Spotlight)));
+ AddStep("remove last room", () => rooms.RemoveAt(rooms.Count - 1));
+ AddAssert("first spotlight still selected", () => checkRoomSelected(rooms.First(r => r.Category == RoomCategory.Spotlight)));
- AddStep("remove spotlight room", () => RoomManager.RemoveRoom(RoomManager.Rooms.Single(r => r.Category == RoomCategory.Spotlight)));
+ AddStep("remove spotlight room", () => rooms.RemoveAll(r => r.Category == RoomCategory.Spotlight));
AddAssert("selection vacated", () => checkRoomSelected(null));
}
[Test]
public void TestKeyboardNavigation()
{
- AddStep("add rooms", () => RoomManager.AddRooms(3));
+ AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3)));
AddAssert("no selection", () => checkRoomSelected(null));
press(Key.Down);
- AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
+ AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room));
press(Key.Up);
- AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
+ AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room));
press(Key.Down);
press(Key.Down);
- AddAssert("last room selected", () => checkRoomSelected(RoomManager.Rooms.Last()));
+ AddAssert("last room selected", () => checkRoomSelected(container.DrawableRooms.Last().Room));
}
[Test]
public void TestKeyboardNavigationAfterOrderChange()
{
- AddStep("add rooms", () => RoomManager.AddRooms(3));
+ AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3)));
AddStep("reorder rooms", () =>
{
- var room = RoomManager.Rooms[1];
+ var room = rooms[1];
+ rooms.Remove(room);
- RoomManager.RemoveRoom(room);
- RoomManager.AddOrUpdateRoom(room);
+ room.RoomID += 3;
+ rooms.Add(room);
});
AddAssert("no selection", () => checkRoomSelected(null));
@@ -116,12 +118,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestClickDeselection()
{
- AddStep("add room", () => RoomManager.AddRooms(1));
+ AddStep("add room", () => rooms.AddRange(GenerateRooms(1)));
AddAssert("no selection", () => checkRoomSelected(null));
press(Key.Down);
- AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
+ AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room));
AddStep("click away", () => InputManager.Click(MouseButton.Left));
AddAssert("no selection", () => checkRoomSelected(null));
@@ -135,11 +137,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestStringFiltering()
{
- AddStep("add rooms", () => RoomManager.AddRooms(4));
+ AddStep("add rooms", () => rooms.AddRange(GenerateRooms(4)));
AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4);
- AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = "1" });
+ AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = rooms.First().Name });
AddUntilStep("1 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 1);
@@ -151,8 +153,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestRulesetFiltering()
{
- AddStep("add rooms", () => RoomManager.AddRooms(2, new OsuRuleset().RulesetInfo));
- AddStep("add rooms", () => RoomManager.AddRooms(3, new CatchRuleset().RulesetInfo));
+ AddStep("add rooms", () => rooms.AddRange(GenerateRooms(2, new OsuRuleset().RulesetInfo)));
+ AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, new CatchRuleset().RulesetInfo)));
// Todo: What even is this case...?
AddStep("set empty filter criteria", () => container.Filter.Value = new FilterCriteria());
@@ -170,8 +172,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("add rooms", () =>
{
- RoomManager.AddRooms(1, withPassword: true);
- RoomManager.AddRooms(1, withPassword: false);
+ rooms.AddRange(GenerateRooms(1, withPassword: true));
+ rooms.AddRange(GenerateRooms(1, withPassword: false));
});
AddStep("apply default filter", () => container.Filter.SetDefault());
@@ -190,7 +192,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestPasswordProtectedRooms()
{
- AddStep("add rooms", () => RoomManager.AddRooms(3, withPassword: true));
+ AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, withPassword: true)));
}
private bool checkRoomSelected(Room? room) => SelectedRoom.Value == room;
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
index eb649acd2d..b4ec9d5858 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
@@ -8,8 +8,8 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge;
-using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Input;
@@ -18,11 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiplayerLoungeSubScreen : MultiplayerTestScene
{
- protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
-
- private LoungeSubScreen loungeScreen = null!;
-
- private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First();
+ private MultiplayerLoungeSubScreen loungeScreen = null!;
public TestSceneMultiplayerLoungeSubScreen()
: base(false)
@@ -40,7 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestJoinRoomWithoutPassword()
{
- AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false));
+ createRooms(GenerateRooms(1, withPassword: false));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
@@ -50,7 +46,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestPopoverHidesOnBackButton()
{
- AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ createRooms(GenerateRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
@@ -70,7 +66,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestPopoverHidesOnLeavingScreen()
{
- AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ createRooms(GenerateRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
@@ -86,7 +82,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
- AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ createRooms(GenerateRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
@@ -105,7 +101,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
- AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ createRooms(GenerateRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
@@ -124,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
- AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ createRooms(GenerateRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
@@ -139,7 +135,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
- AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ createRooms(GenerateRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
@@ -149,6 +145,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("room joined", () => MultiplayerClient.RoomJoined);
}
+ private void createRooms(params Room[] rooms)
+ {
+ AddStep("create rooms", () =>
+ {
+ foreach (var room in rooms)
+ API.Queue(new CreateRoomRequest(room));
+ });
+
+ AddStep("refresh lounge", () => loungeScreen.RefreshRooms());
+ }
+
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies();
}
}
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
index 9d65be2a19..94a81ecdc7 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
@@ -17,8 +17,6 @@ namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene
{
- protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
-
private TestLoungeSubScreen loungeScreen = null!;
public override void SetUpSteps()
@@ -26,7 +24,6 @@ namespace osu.Game.Tests.Visual.Playlists
base.SetUpSteps();
AddStep("push screen", () => LoadScreen(loungeScreen = new TestLoungeSubScreen()));
-
AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen());
}
@@ -35,8 +32,7 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestManyRooms()
{
- AddStep("add rooms", () => RoomManager.AddRooms(500));
- AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 500);
+ createRooms(GenerateRooms(500));
}
[Test]
@@ -44,10 +40,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left));
- AddStep("add rooms", () => RoomManager.AddRooms(30));
- AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30);
-
- AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0]));
+ createRooms(GenerateRooms(30));
AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.DrawableRooms[2]));
AddStep("hold down", () => InputManager.PressButton(MouseButton.Left));
@@ -61,10 +54,7 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestScrollSelectedIntoView()
{
- AddStep("add rooms", () => RoomManager.AddRooms(30));
- AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 30);
-
- AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[0]));
+ createRooms(GenerateRooms(30));
AddStep("select last room", () => roomsContainer.DrawableRooms[^1].TriggerClick());
@@ -75,8 +65,7 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestEnteringRoomTakesLeaseOnSelection()
{
- AddStep("add rooms", () => RoomManager.AddRooms(1));
- AddUntilStep("wait for rooms", () => roomsContainer.DrawableRooms.Count == 1);
+ createRooms(GenerateRooms(1));
AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled);
@@ -95,6 +84,17 @@ namespace osu.Game.Tests.Visual.Playlists
loungeScreen.ChildrenOfType().First().ScreenSpaceDrawQuad
.Contains(room.ScreenSpaceDrawQuad.Centre);
+ private void createRooms(params Room[] rooms)
+ {
+ AddStep("create rooms", () =>
+ {
+ foreach (var room in rooms)
+ API.Queue(new CreateRoomRequest(room));
+ });
+
+ AddStep("refresh lounge", () => loungeScreen.RefreshRooms());
+ }
+
private partial class TestLoungeSubScreen : PlaylistsLoungeSubScreen
{
public new Bindable SelectedRoom => base.SelectedRoom;
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
index 51e39e1b7f..f7b0bc0d58 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
@@ -17,8 +17,6 @@ namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestScenePlaylistsMatchSettingsOverlay : OnlinePlayTestScene
{
- protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
-
private TestRoomSettings settings = null!;
private Func? handleRequest;
diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs
index 5cb4c9420a..1495f97de4 100644
--- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs
@@ -4,17 +4,23 @@
using System;
using System.Linq;
using System.Threading.Tasks;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Game.Online;
+using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
namespace osu.Game.Screens.OnlinePlay.Components
{
///
- /// A that polls for the lounge listing.
+ /// A that polls for the lounge listing.
///
- public partial class ListingPollingComponent : RoomPollingComponent
+ public partial class ListingPollingComponent : PollingComponent
{
+ [Resolved]
+ private IAPIProvider api { get; set; } = null!;
+
public required Action RoomsReceived { get; init; }
public readonly IBindable Filter = new Bindable();
@@ -22,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
protected override Task Poll()
{
- if (!API.IsLoggedIn)
+ if (!api.IsLoggedIn)
return base.Poll();
if (Filter.Value == null)
@@ -41,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
req.Failure += _ => tcs.SetResult(false);
- API.Queue(req);
+ api.Queue(req);
lastPollRequest = req;
diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
deleted file mode 100644
index a1b61ea7a3..0000000000
--- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using osu.Framework.Bindables;
-using osu.Framework.Development;
-using osu.Framework.Graphics;
-using osu.Framework.Logging;
-using osu.Game.Online.Rooms;
-
-namespace osu.Game.Screens.OnlinePlay.Components
-{
- // Todo: This class should be inlined into the lounge.
- public partial class RoomManager : Component, IRoomManager
- {
- public event Action? RoomsUpdated;
-
- private readonly BindableList rooms = new BindableList();
-
- public IBindableList Rooms => rooms;
-
- public RoomManager()
- {
- RelativeSizeAxes = Axes.Both;
- }
-
- private readonly HashSet ignoredRooms = new HashSet();
-
- public void AddOrUpdateRoom(Room room)
- {
- Debug.Assert(ThreadSafety.IsUpdateThread);
- Debug.Assert(room.RoomID != null);
-
- if (ignoredRooms.Contains(room.RoomID.Value))
- return;
-
- try
- {
- var existing = rooms.FirstOrDefault(e => e.RoomID == room.RoomID);
- if (existing == null)
- rooms.Add(room);
- else
- existing.CopyFrom(room);
- }
- catch (Exception ex)
- {
- Logger.Error(ex, $"Failed to update room: {room.Name}.");
-
- ignoredRooms.Add(room.RoomID.Value);
- rooms.Remove(room);
- }
-
- notifyRoomsUpdated();
- }
-
- public void RemoveRoom(Room room)
- {
- Debug.Assert(ThreadSafety.IsUpdateThread);
-
- rooms.Remove(room);
- notifyRoomsUpdated();
- }
-
- public void ClearRooms()
- {
- Debug.Assert(ThreadSafety.IsUpdateThread);
-
- rooms.Clear();
- notifyRoomsUpdated();
- }
-
- private void notifyRoomsUpdated()
- {
- Scheduler.AddOnce(invokeRoomsUpdated);
-
- void invokeRoomsUpdated() => RoomsUpdated?.Invoke();
- }
- }
-}
diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs
deleted file mode 100644
index 0ba7f20f1c..0000000000
--- a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs
+++ /dev/null
@@ -1,18 +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.Allocation;
-using osu.Game.Online;
-using osu.Game.Online.API;
-
-namespace osu.Game.Screens.OnlinePlay.Components
-{
- public abstract partial class RoomPollingComponent : PollingComponent
- {
- [Resolved]
- protected IAPIProvider API { get; private set; } = null!;
-
- [Resolved]
- protected IRoomManager RoomManager { get; private set; } = null!;
- }
-}
diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs
index f04fd6a096..bfa059f72e 100644
--- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs
@@ -2,15 +2,21 @@
// See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
+using osu.Framework.Allocation;
+using osu.Game.Online;
+using osu.Game.Online.API;
using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Components
{
///
- /// A that polls for the currently-selected room.
+ /// A that polls for and updates a room.
///
- public partial class SelectionPollingComponent : RoomPollingComponent
+ public partial class SelectionPollingComponent : PollingComponent
{
+ [Resolved]
+ private IAPIProvider api { get; set; } = null!;
+
private readonly Room room;
public SelectionPollingComponent(Room room)
@@ -22,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
protected override Task Poll()
{
- if (!API.IsLoggedIn)
+ if (!api.IsLoggedIn)
return base.Poll();
if (room.RoomID == null)
@@ -41,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
req.Failure += _ => tcs.SetResult(false);
- API.Queue(req);
+ api.Queue(req);
lastPollRequest = req;
diff --git a/osu.Game/Screens/OnlinePlay/IRoomManager.cs b/osu.Game/Screens/OnlinePlay/IRoomManager.cs
deleted file mode 100644
index 8ecb1dd7e0..0000000000
--- a/osu.Game/Screens/OnlinePlay/IRoomManager.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.Allocation;
-using osu.Framework.Bindables;
-using osu.Game.Online.Rooms;
-
-namespace osu.Game.Screens.OnlinePlay
-{
- [Cached(typeof(IRoomManager))]
- public interface IRoomManager
- {
- ///
- /// Invoked when the s have been updated.
- ///
- event Action RoomsUpdated;
-
- ///
- /// All the active s.
- ///
- IBindableList Rooms { get; }
-
- ///
- /// Adds a to this .
- /// If already existing, the local room will be updated with the given one.
- ///
- /// The incoming .
- void AddOrUpdateRoom(Room room);
-
- ///
- /// Removes a from this .
- ///
- /// The to remove.
- void RemoveRoom(Room room);
-
- ///
- /// Removes all s from this .
- ///
- void ClearRooms();
- }
-}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
index 78501a56d7..6c383f1bf6 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
@@ -54,6 +54,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
};
protected readonly Bindable SelectedRoom = new Bindable();
+ protected readonly BindableList Rooms = new BindableList();
[Resolved]
private MusicController music { get; set; } = null!;
@@ -76,7 +77,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private IDisposable? joiningRoomOperation;
private LeasedBindable? selectionLease;
- private readonly BindableList rooms = new BindableList();
private readonly Bindable filter = new Bindable();
private readonly Bindable hasListingResults = new Bindable();
private readonly IBindable operationInProgress = new Bindable();
@@ -121,7 +121,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
ScrollbarOverlapsContent = false,
Child = roomsContainer = new RoomsContainer
{
- Rooms = { BindTarget = rooms },
+ Rooms = { BindTarget = Rooms },
SelectedRoom = { BindTarget = SelectedRoom },
Filter = { BindTarget = filter },
}
@@ -208,7 +208,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
filter.BindValueChanged(_ =>
{
- rooms.Clear();
+ Rooms.Clear();
hasListingResults.Value = false;
listingPollingComponent.PollImmediately();
});
@@ -218,11 +218,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private void onListingReceived(Room[] result)
{
- Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value);
+ Dictionary localRoomsById = Rooms.ToDictionary(r => r.RoomID!.Value);
Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value);
// Remove all local rooms no longer in the result set.
- rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value));
+ Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value));
// Add or update local rooms with the result set.
foreach (var r in result)
@@ -230,7 +230,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom))
existingRoom.CopyFrom(r);
else
- rooms.Add(r);
+ Rooms.Add(r);
}
hasListingResults.Value = true;
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
index 8988c82dee..812e42479b 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
@@ -11,7 +11,6 @@ using osu.Game.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Screens.Menu;
-using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Users;
@@ -39,9 +38,6 @@ namespace osu.Game.Screens.OnlinePlay
[Cached]
private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker();
- [Cached(Type = typeof(IRoomManager))]
- private readonly RoomManager roomManager = new RoomManager();
-
[Resolved]
protected IAPIProvider API { get; private set; } = null!;
@@ -65,7 +61,6 @@ namespace osu.Game.Screens.OnlinePlay
{
screenStack,
new Header(ScreenTitle, screenStack),
- roomManager,
ongoingOperationTracker,
}
};
diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs
index 8ddc5325db..5780cf6eff 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs
@@ -18,11 +18,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay
///
Bindable SelectedRoom { get; }
- ///
- /// The cached
- ///
- IRoomManager RoomManager { get; }
-
///
/// The cached .
///
diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
index 3f6c175fbd..c3a5e1c3ec 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
@@ -10,7 +10,9 @@ using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
+using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay;
namespace osu.Game.Tests.Visual.OnlinePlay
@@ -21,7 +23,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay
public abstract partial class OnlinePlayTestScene : ScreenTestScene, IOnlinePlayTestSceneDependencies
{
public Bindable SelectedRoom => OnlinePlayDependencies.SelectedRoom;
- public IRoomManager RoomManager => OnlinePlayDependencies.RoomManager;
public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies.OngoingOperationTracker;
public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies.AvailabilityTracker;
public TestUserLookupCache UserLookupCache => OnlinePlayDependencies.UserLookupCache;
@@ -34,9 +35,13 @@ namespace osu.Game.Tests.Visual.OnlinePlay
protected override Container Content => content;
+ [Resolved]
+ private RulesetStore rulesets { get; set; } = null!;
+
private readonly Container content;
private readonly Container drawableDependenciesContainer;
private DelegatedDependencyContainer dependencies = null!;
+ private int currentRoomId;
protected OnlinePlayTestScene()
{
@@ -93,6 +98,31 @@ namespace osu.Game.Tests.Visual.OnlinePlay
///
protected virtual OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new OnlinePlayTestSceneDependencies();
+ protected Room[] GenerateRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false)
+ {
+ Room[] rooms = new Room[count];
+
+ // Can't reference Osu ruleset project here.
+ ruleset ??= rulesets.GetRuleset(0)!;
+
+ for (int i = 0; i < count; i++)
+ {
+ rooms[i] = new Room
+ {
+ RoomID = currentRoomId++,
+ Name = $@"Room {currentRoomId}",
+ Host = new APIUser { Username = @"Host" },
+ Duration = TimeSpan.FromSeconds(10),
+ Category = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal,
+ Password = withPassword ? @"password" : null,
+ PlaylistItemStats = new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] },
+ Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }]
+ };
+ }
+
+ return rooms;
+ }
+
///
/// A providing a mutable lookup source for online play dependencies.
///
diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs
index 203922c057..cc448beea0 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs
@@ -19,7 +19,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay
public class OnlinePlayTestSceneDependencies : IReadOnlyDependencyContainer, IOnlinePlayTestSceneDependencies
{
public Bindable SelectedRoom { get; }
- public IRoomManager RoomManager { get; }
public OngoingOperationTracker OngoingOperationTracker { get; }
public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; }
public TestRoomRequestsHandler RequestsHandler { get; }
@@ -40,7 +39,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay
RequestsHandler = new TestRoomRequestsHandler();
OngoingOperationTracker = new OngoingOperationTracker();
AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
- RoomManager = new TestRoomManager();
UserLookupCache = new TestUserLookupCache();
BeatmapLookupCache = new BeatmapLookupCache();
@@ -48,7 +46,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay
CacheAs(RequestsHandler);
CacheAs(SelectedRoom);
- CacheAs(RoomManager);
CacheAs(OngoingOperationTracker);
CacheAs(AvailabilityTracker);
CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum));
diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs
deleted file mode 100644
index bff2753929..0000000000
--- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs
+++ /dev/null
@@ -1,59 +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.Allocation;
-using osu.Game.Beatmaps;
-using osu.Game.Online.API;
-using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Rooms;
-using osu.Game.Rulesets;
-using osu.Game.Screens.OnlinePlay.Components;
-
-namespace osu.Game.Tests.Visual.OnlinePlay
-{
- ///
- /// A very simple for use in online play test scenes.
- ///
- public partial class TestRoomManager : RoomManager
- {
- private int currentRoomId;
-
- [Resolved]
- private IAPIProvider api { get; set; } = null!;
-
- [Resolved]
- private RulesetStore rulesets { get; set; } = null!;
-
- public void AddRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false)
- {
- // Can't reference Osu ruleset project here.
- ruleset ??= rulesets.GetRuleset(0)!;
-
- for (int i = 0; i < count; i++)
- {
- AddRoom(new Room
- {
- Name = $@"Room {currentRoomId}",
- Host = new APIUser { Username = @"Host" },
- Duration = TimeSpan.FromSeconds(10),
- Category = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal,
- Password = withPassword ? @"password" : null,
- PlaylistItemStats = new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] },
- Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }]
- });
- }
- }
-
- public void AddRoom(Room room)
- {
- room.RoomID = -currentRoomId;
-
- var req = new CreateRoomRequest(room);
- req.Success += AddOrUpdateRoom;
- api.Queue(req);
-
- currentRoomId++;
- }
- }
-}
diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
index c9149bda22..63bc9325fa 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
@@ -36,8 +36,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
private int currentScoreId = 1;
///
- /// Handles an API request, while also updating the local state to match
- /// how the server would eventually respond and update an .
+ /// Handles an API request, while also updating the local state to match how the server would eventually respond.
///
/// The API request to handle.
/// The local user to store in responses where required.
From 1b07b6d16f49fd06572c3366685a08f2a2641669 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 12 Feb 2025 21:48:59 +0900
Subject: [PATCH 081/262] Remove selected room leasing, make bindables private
I believe once upon a time the `SelectedRoom` bindable used to be bound
to `RoomManager.JoinedRoom` or similar. But now it's effectively private
to the lounge subscreen and so a lease is unnecessary.
---
.../TestScenePlaylistsLoungeSubScreen.cs | 28 +------------
.../OnlinePlay/Lounge/LoungeSubScreen.cs | 39 ++++++-------------
2 files changed, 13 insertions(+), 54 deletions(-)
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
index 94a81ecdc7..35bf6dc28a 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
@@ -3,7 +3,6 @@
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Bindables;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
@@ -17,13 +16,13 @@ namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene
{
- private TestLoungeSubScreen loungeScreen = null!;
+ private PlaylistsLoungeSubScreen loungeScreen = null!;
public override void SetUpSteps()
{
base.SetUpSteps();
- AddStep("push screen", () => LoadScreen(loungeScreen = new TestLoungeSubScreen()));
+ AddStep("push screen", () => LoadScreen(loungeScreen = new PlaylistsLoungeSubScreen()));
AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen());
}
@@ -62,24 +61,6 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.DrawableRooms[^1]));
}
- [Test]
- public void TestEnteringRoomTakesLeaseOnSelection()
- {
- createRooms(GenerateRooms(1));
-
- AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled);
-
- AddStep("select room", () => roomsContainer.DrawableRooms[0].TriggerClick());
- AddAssert("selected room is non-null", () => loungeScreen.SelectedRoom.Value != null);
-
- AddStep("enter room", () => roomsContainer.DrawableRooms[0].TriggerClick());
-
- AddUntilStep("wait for match load", () => Stack.CurrentScreen is PlaylistsRoomSubScreen);
-
- AddAssert("selected room is non-null", () => loungeScreen.SelectedRoom.Value != null);
- AddAssert("selected room is disabled", () => loungeScreen.SelectedRoom.Disabled);
- }
-
private bool checkRoomVisible(DrawableRoom room) =>
loungeScreen.ChildrenOfType().First().ScreenSpaceDrawQuad
.Contains(room.ScreenSpaceDrawQuad.Centre);
@@ -94,10 +75,5 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("refresh lounge", () => loungeScreen.RefreshRooms());
}
-
- private partial class TestLoungeSubScreen : PlaylistsLoungeSubScreen
- {
- public new Bindable SelectedRoom => base.SelectedRoom;
- }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
index 6c383f1bf6..7bb0c67990 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen
{
- SelectedRoom = { BindTarget = SelectedRoom }
+ SelectedRoom = { BindTarget = selectedRoom }
};
protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby();
@@ -53,9 +53,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
AutoSizeAxes = Axes.Both
};
- protected readonly Bindable SelectedRoom = new Bindable();
- protected readonly BindableList Rooms = new BindableList();
-
[Resolved]
private MusicController music { get; set; } = null!;
@@ -75,8 +72,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
protected OsuConfigManager Config { get; private set; } = null!;
private IDisposable? joiningRoomOperation;
- private LeasedBindable? selectionLease;
+ private readonly Bindable selectedRoom = new Bindable();
+ private readonly BindableList rooms = new BindableList();
private readonly Bindable filter = new Bindable();
private readonly Bindable hasListingResults = new Bindable();
private readonly IBindable operationInProgress = new Bindable();
@@ -121,8 +119,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
ScrollbarOverlapsContent = false,
Child = roomsContainer = new RoomsContainer
{
- Rooms = { BindTarget = Rooms },
- SelectedRoom = { BindTarget = SelectedRoom },
+ Rooms = { BindTarget = rooms },
+ SelectedRoom = { BindTarget = selectedRoom },
Filter = { BindTarget = filter },
}
},
@@ -182,7 +180,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
};
// scroll selected room into view on selection.
- SelectedRoom.BindValueChanged(val =>
+ selectedRoom.BindValueChanged(val =>
{
var drawable = roomsContainer.DrawableRooms.FirstOrDefault(r => r.Room == val.NewValue);
if (drawable != null)
@@ -208,7 +206,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
filter.BindValueChanged(_ =>
{
- Rooms.Clear();
+ rooms.Clear();
hasListingResults.Value = false;
listingPollingComponent.PollImmediately();
});
@@ -218,11 +216,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private void onListingReceived(Room[] result)
{
- Dictionary localRoomsById = Rooms.ToDictionary(r => r.RoomID!.Value);
+ Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value);
Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value);
// Remove all local rooms no longer in the result set.
- Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value));
+ rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value));
// Add or update local rooms with the result set.
foreach (var r in result)
@@ -230,7 +228,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom))
existingRoom.CopyFrom(r);
else
- Rooms.Add(r);
+ rooms.Add(r);
}
hasListingResults.Value = true;
@@ -286,14 +284,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{
base.OnResuming(e);
- Debug.Assert(selectionLease != null);
-
- selectionLease.Return();
- selectionLease = null;
-
- if (SelectedRoom.Value?.RoomID == null)
- SelectedRoom.Value = new Room();
-
music.EnsurePlayingSomething();
onReturning();
@@ -415,14 +405,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
OpenNewRoom(room ?? CreateNewRoom());
});
- protected virtual void OpenNewRoom(Room room)
- {
- selectionLease = SelectedRoom.BeginLease(false);
- Debug.Assert(selectionLease != null);
- selectionLease.Value = room;
-
- this.Push(CreateRoomSubScreen(room));
- }
+ protected virtual void OpenNewRoom(Room room) => this.Push(CreateRoomSubScreen(room));
public void RefreshRooms() => listingPollingComponent.PollImmediately();
From 74ccac37ae665ea2a9a603316077453520a8b9de Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 12 Feb 2025 21:57:18 +0900
Subject: [PATCH 082/262] Encapsulate RoomsContainer scroll a bit better
---
.../TestSceneLoungeRoomsContainer.cs | 4 +--
.../Lounge/Components/RoomsContainer.cs | 35 ++++++++++++-------
.../OnlinePlay/Lounge/LoungeSubScreen.cs | 26 +++-----------
3 files changed, 30 insertions(+), 35 deletions(-)
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
index 9daad960c7..772eb91174 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
@@ -32,13 +32,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
rooms = new BindableList();
Child = new PopoverContainer
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
Child = container = new RoomsContainer
{
+ RelativeSizeAxes = Axes.Both,
Rooms = { BindTarget = rooms },
SelectedRoom = { BindTarget = SelectedRoom }
}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
index 6681cbe720..65f969bc7b 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Input.Bindings;
using osu.Game.Online.Rooms;
@@ -28,6 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
public IReadOnlyList DrawableRooms => roomFlow.FlowingChildren.Cast().ToArray();
+ private readonly ScrollContainer scroll;
private readonly FillFlowContainer roomFlow;
// handle deselection
@@ -35,28 +37,29 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
public RoomsContainer()
{
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
-
- // account for the fact we are in a scroll container and want a bit of spacing from the scroll bar.
- Padding = new MarginPadding { Right = 5 };
-
- InternalChild = new OsuContextMenuContainer
+ InternalChild = scroll = new OsuScrollContainer
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Child = roomFlow = new FillFlowContainer
+ RelativeSizeAxes = Axes.Both,
+ ScrollbarOverlapsContent = false,
+ Padding = new MarginPadding { Right = 5 },
+ Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(10),
+ Child = roomFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(10),
+ }
}
};
}
protected override void LoadComplete()
{
+ SelectedRoom.BindValueChanged(onSelectedRoomChanged, true);
Rooms.BindCollectionChanged(roomsChanged, true);
Filter.BindValueChanged(criteria => applyFilterCriteria(criteria.NewValue), true);
}
@@ -119,6 +122,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
}
}
+ private void onSelectedRoomChanged(ValueChangedEvent room)
+ {
+ // scroll selected room into view on selection.
+ var drawable = DrawableRooms.FirstOrDefault(r => r.Room == room.NewValue);
+ if (drawable != null)
+ scroll.ScrollIntoView(drawable);
+ }
+
private void roomsChanged(object? sender, NotifyCollectionChangedEventArgs args)
{
switch (args.Action)
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
index 7bb0c67990..1877244c03 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
@@ -17,7 +17,6 @@ using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Configuration;
-using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Online.API;
@@ -82,7 +81,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private ListingPollingComponent listingPollingComponent = null!;
private PopoverContainer popoverContainer = null!;
private LoadingLayer loadingLayer = null!;
- private RoomsContainer roomsContainer = null!;
private SearchTextBox searchTextBox = null!;
protected Dropdown StatusDropdown { get; private set; } = null!;
@@ -95,8 +93,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
if (idleTracker != null)
isIdle.BindTo(idleTracker.IsIdle);
- OsuScrollContainer scrollContainer;
-
InternalChildren = new Drawable[]
{
listingPollingComponent = new ListingPollingComponent
@@ -113,17 +109,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
Horizontal = WaveOverlayContainer.WIDTH_PADDING,
Top = Header.HEIGHT + controls_area_height + 20,
},
- Child = scrollContainer = new OsuScrollContainer
+ Child = new RoomsContainer
{
RelativeSizeAxes = Axes.Both,
- ScrollbarOverlapsContent = false,
- Child = roomsContainer = new RoomsContainer
- {
- Rooms = { BindTarget = rooms },
- SelectedRoom = { BindTarget = selectedRoom },
- Filter = { BindTarget = filter },
- }
- },
+ Rooms = { BindTarget = rooms },
+ SelectedRoom = { BindTarget = selectedRoom },
+ Filter = { BindTarget = filter },
+ }
},
loadingLayer = new LoadingLayer(true),
new FillFlowContainer
@@ -178,14 +170,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
},
},
};
-
- // scroll selected room into view on selection.
- selectedRoom.BindValueChanged(val =>
- {
- var drawable = roomsContainer.DrawableRooms.FirstOrDefault(r => r.Room == val.NewValue);
- if (drawable != null)
- scrollContainer.ScrollIntoView(drawable);
- });
}
protected override void LoadComplete()
From 43928c94db5b4695b2baab8acfb41d58198322aa Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 12 Feb 2025 22:03:22 +0900
Subject: [PATCH 083/262] Remove remaining bindables
---
.../OnlinePlay/Lounge/LoungeSubScreen.cs | 17 +++++++----------
.../Multiplayer/MultiplayerLoungeSubScreen.cs | 3 ---
.../OnlinePlay/TestRoomRequestsHandler.cs | 2 --
3 files changed, 7 insertions(+), 15 deletions(-)
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
index 1877244c03..2e78e88ccf 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
protected override BackgroundScreen CreateBackground() => new LoungeBackgroundScreen
{
- SelectedRoom = { BindTarget = selectedRoom }
+ SelectedRoom = { BindTarget = roomsContainer.SelectedRoom }
};
protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby();
@@ -72,12 +72,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private IDisposable? joiningRoomOperation;
- private readonly Bindable selectedRoom = new Bindable();
- private readonly BindableList rooms = new BindableList();
private readonly Bindable filter = new Bindable();
private readonly Bindable hasListingResults = new Bindable();
private readonly IBindable operationInProgress = new Bindable();
private readonly IBindable isIdle = new BindableBool();
+ private RoomsContainer roomsContainer = null!;
private ListingPollingComponent listingPollingComponent = null!;
private PopoverContainer popoverContainer = null!;
private LoadingLayer loadingLayer = null!;
@@ -109,11 +108,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
Horizontal = WaveOverlayContainer.WIDTH_PADDING,
Top = Header.HEIGHT + controls_area_height + 20,
},
- Child = new RoomsContainer
+ Child = roomsContainer = new RoomsContainer
{
RelativeSizeAxes = Axes.Both,
- Rooms = { BindTarget = rooms },
- SelectedRoom = { BindTarget = selectedRoom },
Filter = { BindTarget = filter },
}
},
@@ -190,7 +187,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
filter.BindValueChanged(_ =>
{
- rooms.Clear();
+ roomsContainer.Rooms.Clear();
hasListingResults.Value = false;
listingPollingComponent.PollImmediately();
});
@@ -200,11 +197,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private void onListingReceived(Room[] result)
{
- Dictionary localRoomsById = rooms.ToDictionary(r => r.RoomID!.Value);
+ Dictionary localRoomsById = roomsContainer.Rooms.ToDictionary(r => r.RoomID!.Value);
Dictionary resultRoomsById = result.ToDictionary(r => r.RoomID!.Value);
// Remove all local rooms no longer in the result set.
- rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value));
+ roomsContainer.Rooms.RemoveAll(r => !resultRoomsById.ContainsKey(r.RoomID!.Value));
// Add or update local rooms with the result set.
foreach (var r in result)
@@ -212,7 +209,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
if (localRoomsById.TryGetValue(r.RoomID!.Value, out Room? existingRoom))
existingRoom.CopyFrom(r);
else
- rooms.Add(r);
+ roomsContainer.Rooms.Add(r);
}
hasListingResults.Value = true;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
index 3cf873ec78..6191cfd975 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
@@ -3,9 +3,7 @@
using System;
using System.Collections.Generic;
-using System.Threading.Tasks;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Logging;
using osu.Framework.Graphics;
@@ -15,7 +13,6 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
-using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
index 63bc9325fa..617a4cff79 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
@@ -15,7 +15,6 @@ using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
-using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Utils;
@@ -28,7 +27,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay
public class TestRoomRequestsHandler
{
public IReadOnlyList ServerSideRooms => serverSideRooms;
-
private readonly List serverSideRooms = new List();
private int currentRoomId = 1;
From 24cc77287e5e715a0fc684999f0a9aadd1355380 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 12 Feb 2025 22:21:04 +0900
Subject: [PATCH 084/262] Refactor polling components (namespace/namings)
---
.../Visual/Multiplayer/TestSceneMultiplayer.cs | 3 +--
.../LoungePollingComponent.cs} | 4 ++--
.../OnlinePlay/Lounge/LoungeSubScreen.cs | 17 ++++++++---------
.../Playlists/PlaylistsRoomSubScreen.cs | 8 ++++----
.../PlaylistsRoomUpdater.cs} | 6 +++---
5 files changed, 18 insertions(+), 20 deletions(-)
rename osu.Game/Screens/OnlinePlay/{Components/ListingPollingComponent.cs => Lounge/LoungePollingComponent.cs} (92%)
rename osu.Game/Screens/OnlinePlay/{Components/SelectionPollingComponent.cs => Playlists/PlaylistsRoomUpdater.cs} (88%)
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 0966c61a3a..a87216287d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -33,7 +33,6 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay;
-using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
@@ -806,7 +805,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for room", () => this.ChildrenOfType().Any());
AddStep("select room", () => InputManager.Key(Key.Down));
- AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0);
+ AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0);
AddStep("change server-side settings", () =>
{
multiplayerClient.ServerSideRooms[0].Name = "New name";
diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs
similarity index 92%
rename from osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs
rename to osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs
index 1495f97de4..420a96cf8a 100644
--- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungePollingComponent.cs
@@ -11,12 +11,12 @@ using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
-namespace osu.Game.Screens.OnlinePlay.Components
+namespace osu.Game.Screens.OnlinePlay.Lounge
{
///
/// A that polls for the lounge listing.
///
- public partial class ListingPollingComponent : PollingComponent
+ public partial class LoungePollingComponent : PollingComponent
{
[Resolved]
private IAPIProvider api { get; set; } = null!;
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
index 2e78e88ccf..3a4da96ba1 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
@@ -24,7 +24,6 @@ using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets;
-using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Users;
@@ -77,7 +76,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private readonly IBindable operationInProgress = new Bindable();
private readonly IBindable isIdle = new BindableBool();
private RoomsContainer roomsContainer = null!;
- private ListingPollingComponent listingPollingComponent = null!;
+ private LoungePollingComponent pollingComponent = null!;
private PopoverContainer popoverContainer = null!;
private LoadingLayer loadingLayer = null!;
private SearchTextBox searchTextBox = null!;
@@ -94,7 +93,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
InternalChildren = new Drawable[]
{
- listingPollingComponent = new ListingPollingComponent
+ pollingComponent = new LoungePollingComponent
{
RoomsReceived = onListingReceived,
Filter = { BindTarget = filter }
@@ -189,7 +188,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{
roomsContainer.Rooms.Clear();
hasListingResults.Value = false;
- listingPollingComponent.PollImmediately();
+ pollingComponent.PollImmediately();
});
updateFilter();
@@ -270,7 +269,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
onReturning();
// Poll for any newly-created rooms (including potentially the user's own).
- listingPollingComponent.PollImmediately();
+ pollingComponent.PollImmediately();
}
public override bool OnExiting(ScreenExitEvent e)
@@ -388,7 +387,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
protected virtual void OpenNewRoom(Room room) => this.Push(CreateRoomSubScreen(room));
- public void RefreshRooms() => listingPollingComponent.PollImmediately();
+ public void RefreshRooms() => pollingComponent.PollImmediately();
private void updateLoadingLayer()
{
@@ -401,11 +400,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private void updatePollingRate(bool isCurrentScreen)
{
if (!isCurrentScreen)
- listingPollingComponent.TimeBetweenPolls.Value = 0;
+ pollingComponent.TimeBetweenPolls.Value = 0;
else
- listingPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000;
+ pollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000;
- Logger.Log($"Polling adjusted (listing: {listingPollingComponent.TimeBetweenPolls.Value})");
+ Logger.Log($"Polling adjusted (listing: {pollingComponent.TimeBetweenPolls.Value})");
}
protected abstract OsuButton CreateNewRoomButton();
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs
index bf0e428483..a74ae642fb 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private IdleTracker? idleTracker { get; set; }
private MatchLeaderboard leaderboard = null!;
- private SelectionPollingComponent selectionPollingComponent = null!;
+ private PlaylistsRoomUpdater roomUpdater = null!;
private FillFlowContainer progressSection = null!;
private DrawableRoomPlaylist drawablePlaylist = null!;
@@ -64,7 +64,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
if (idleTracker != null)
isIdle.BindTo(idleTracker.IsIdle);
- AddInternal(selectionPollingComponent = new SelectionPollingComponent(Room));
+ AddInternal(roomUpdater = new PlaylistsRoomUpdater(Room));
}
protected override void LoadComplete()
@@ -328,8 +328,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private void updatePollingRate()
{
- selectionPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000;
- Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})");
+ roomUpdater.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000;
+ Logger.Log($"Polling adjusted (selection: {roomUpdater.TimeBetweenPolls.Value})");
}
private void closePlaylist()
diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomUpdater.cs
similarity index 88%
rename from osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs
rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomUpdater.cs
index bfa059f72e..f68703750a 100644
--- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomUpdater.cs
@@ -7,19 +7,19 @@ using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
-namespace osu.Game.Screens.OnlinePlay.Components
+namespace osu.Game.Screens.OnlinePlay.Playlists
{
///
/// A that polls for and updates a room.
///
- public partial class SelectionPollingComponent : PollingComponent
+ public partial class PlaylistsRoomUpdater : PollingComponent
{
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly Room room;
- public SelectionPollingComponent(Room room)
+ public PlaylistsRoomUpdater(Room room)
{
this.room = room;
}
From 205d6ecffbc989d75c1a32e53a29a9342b88c175 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 12 Feb 2025 22:51:25 +0900
Subject: [PATCH 085/262] Remove `SelectedRoom` abstraction from
`OnlinePlayTestScene`
---
.../StatefulMultiplayerClientTest.cs | 6 ++
.../TestSceneDrawableRoomParticipantsList.cs | 15 +++--
.../TestSceneLoungeRoomsContainer.cs | 7 +-
.../TestSceneMatchBeatmapDetailArea.cs | 10 +--
.../Multiplayer/TestSceneMatchLeaderboard.cs | 4 +-
.../TestSceneMultiSpectatorLeaderboard.cs | 2 +
.../TestSceneMultiSpectatorScreen.cs | 6 +-
.../TestSceneMultiplayerLoungeSubScreen.cs | 5 --
.../TestSceneMultiplayerMatchSongSelect.cs | 13 +++-
.../TestSceneMultiplayerMatchSubScreen.cs | 28 ++++----
.../TestSceneMultiplayerParticipantsList.cs | 6 +-
.../Multiplayer/TestSceneMultiplayerPlayer.cs | 6 ++
.../TestSceneMultiplayerPlaylist.cs | 5 +-
.../TestSceneMultiplayerQueueList.cs | 5 +-
.../TestSceneMultiplayerSpectateButton.cs | 11 +--
.../TestScenePlaylistsSongSelect.cs | 23 ++++---
.../TestScenePlaylistsMatchSettingsOverlay.cs | 29 ++++----
.../TestScenePlaylistsParticipantsList.cs | 10 +--
.../TestScenePlaylistsRoomCreation.cs | 12 ++--
.../Multiplayer/MultiplayerTestScene.cs | 67 ++++++++++---------
.../IOnlinePlayTestSceneDependencies.cs | 6 --
.../Visual/OnlinePlay/OnlinePlayTestScene.cs | 2 -
.../OnlinePlayTestSceneDependencies.cs | 4 --
23 files changed, 149 insertions(+), 133 deletions(-)
diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
index be30e06ed4..c0ca387260 100644
--- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
+++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
@@ -15,6 +15,12 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
[HeadlessTest]
public partial class StatefulMultiplayerClientTest : MultiplayerTestScene
{
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+ JoinDefaultRoom();
+ }
+
[Test]
public void TestUserAddedOnJoin()
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs
index c1662bf944..2fd1268c8a 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs
@@ -15,6 +15,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneDrawableRoomParticipantsList : OnlinePlayTestScene
{
+ private Room room = null!;
private DrawableRoomParticipantsList list = null!;
public override void SetUpSteps()
@@ -23,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create list", () =>
{
- SelectedRoom.Value = new Room
+ room = new Room
{
Name = "test room",
Host = new APIUser
@@ -33,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
};
- Child = list = new DrawableRoomParticipantsList(SelectedRoom.Value)
+ Child = list = new DrawableRoomParticipantsList(room)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -119,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4);
AddAssert("46 hidden users", () => list.ChildrenOfType().Single().Count == 46);
- AddStep("remove from end", () => removeUserAt(SelectedRoom.Value!.RecentParticipants.Count - 1));
+ AddStep("remove from end", () => removeUserAt(room.RecentParticipants.Count - 1));
AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4);
AddAssert("45 hidden users", () => list.ChildrenOfType().Single().Count == 45);
@@ -138,18 +139,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void addUser(int id)
{
- SelectedRoom.Value!.RecentParticipants = SelectedRoom.Value!.RecentParticipants.Append(new APIUser
+ room.RecentParticipants = room.RecentParticipants.Append(new APIUser
{
Id = id,
Username = $"User {id}"
}).ToArray();
- SelectedRoom.Value!.ParticipantCount++;
+ room.ParticipantCount++;
}
private void removeUserAt(int index)
{
- SelectedRoom.Value!.RecentParticipants = SelectedRoom.Value!.RecentParticipants.Where(u => !u.Equals(SelectedRoom.Value!.RecentParticipants[index])).ToArray();
- SelectedRoom.Value!.ParticipantCount--;
+ room.RecentParticipants = room.RecentParticipants.Where(u => !u.Equals(room.RecentParticipants[index])).ToArray();
+ room.ParticipantCount--;
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
index 772eb91174..e83a966144 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
@@ -21,6 +21,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene
{
private BindableList rooms = null!;
+ private Bindable selectedRoom = null!;
private RoomsContainer container = null!;
public override void SetUpSteps()
@@ -30,6 +31,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create container", () =>
{
rooms = new BindableList();
+ selectedRoom = new Bindable();
+
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
@@ -40,7 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
RelativeSizeAxes = Axes.Both,
Rooms = { BindTarget = rooms },
- SelectedRoom = { BindTarget = SelectedRoom }
+ SelectedRoom = { BindTarget = selectedRoom }
}
};
});
@@ -195,7 +198,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, withPassword: true)));
}
- private bool checkRoomSelected(Room? room) => SelectedRoom.Value == room;
+ private bool checkRoomSelected(Room? room) => selectedRoom.Value == room;
private Room? getRoomInFlow(int index) =>
(container.ChildrenOfType>().First().FlowingChildren.ElementAt(index) as DrawableRoom)?.Room;
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
index 813a420cbd..e372d63fde 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
@@ -16,15 +16,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene
{
+ private Room room = null!;
+
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("create area", () =>
{
- SelectedRoom.Value = new Room();
-
- Child = new MatchBeatmapDetailArea(SelectedRoom.Value)
+ Child = new MatchBeatmapDetailArea(room = new Room())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createNewItem()
{
- SelectedRoom.Value!.Playlist = SelectedRoom.Value.Playlist.Append(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
+ room.Playlist = room.Playlist.Append(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
- ID = SelectedRoom.Value.Playlist.Count,
+ ID = room.Playlist.Count,
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
RequiredMods = new[]
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs
index 38522db4d4..39ad21d0b0 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs
@@ -61,9 +61,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create leaderboard", () =>
{
- SelectedRoom.Value = new Room { RoomID = 3 };
-
- Child = new MatchLeaderboard(SelectedRoom.Value)
+ Child = new MatchLeaderboard(new Room { RoomID = 3 })
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
index 3245b3c6a9..1821c2f3bc 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
@@ -24,6 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
base.SetUpSteps();
+ JoinDefaultRoom();
+
AddStep("reset", () =>
{
leaderboard?.RemoveAndDisposeImmediately();
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
index 0a3d48828e..6cbd8a3fed 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
@@ -17,6 +17,7 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
+using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
@@ -42,6 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private BeatmapManager beatmapManager { get; set; } = null!;
private MultiSpectatorScreen spectatorScreen = null!;
+ private Room room = null!;
private readonly List playingUsers = new List();
@@ -63,6 +65,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
base.SetUpSteps();
AddStep("clear playing users", () => playingUsers.Clear());
+
+ JoinDefaultRoom(r => room = r);
}
[TestCase(1)]
@@ -455,7 +459,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
applyToBeatmap?.Invoke(Beatmap.Value);
- LoadScreen(spectatorScreen = new MultiSpectatorScreen(SelectedRoom.Value!, playingUsers.ToArray()));
+ LoadScreen(spectatorScreen = new MultiSpectatorScreen(room, playingUsers.ToArray()));
});
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded));
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
index b4ec9d5858..56187f8778 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
@@ -20,11 +20,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private MultiplayerLoungeSubScreen loungeScreen = null!;
- public TestSceneMultiplayerLoungeSubScreen()
- : base(false)
- {
- }
-
public override void SetUpSteps()
{
base.SetUpSteps();
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 298e6e1b3c..287d7f5816 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private TestMultiplayerMatchSongSelect songSelect = null!;
private Live importedBeatmapSet = null!;
+ private Room room = null!;
[Resolved]
private OsuConfigManager configManager { get; set; } = null!;
@@ -58,6 +59,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
Add(beatmapStore);
}
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+ JoinDefaultRoom(r => room = r);
+ }
+
private void setUp()
{
AddStep("create song select", () =>
@@ -66,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Beatmap.SetDefault();
SelectedMods.SetDefault();
- LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!));
+ LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(room));
});
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
@@ -138,8 +145,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create song select", () =>
{
- SelectedRoom.Value!.Playlist.Single().RulesetID = 2;
- songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value, SelectedRoom.Value.Playlist.Single());
+ room.Playlist.Single().RulesetID = 2;
+ songSelect = new TestMultiplayerMatchSongSelect(room, room.Playlist.Single());
songSelect.OnLoadComplete += _ => Ruleset.Value = new TaikoRuleset().RulesetInfo;
LoadScreen(songSelect);
});
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
index e95209f993..18e926ca5d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
@@ -43,11 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private MultiplayerMatchSubScreen screen = null!;
private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!;
-
- public TestSceneMultiplayerMatchSubScreen()
- : base(false)
- {
- }
+ private Room room = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@@ -66,8 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("load match", () =>
{
- SelectedRoom.Value = new Room { Name = "Test Room" };
- LoadScreen(screen = new TestMultiplayerMatchSubScreen(SelectedRoom.Value!));
+ room = new Room { Name = "Test Room" };
+ LoadScreen(screen = new TestMultiplayerMatchSubScreen(room));
});
AddUntilStep("wait for load", () => screen.IsCurrentScreen());
@@ -78,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("add playlist item", () =>
{
- SelectedRoom.Value!.Playlist =
+ room.Playlist =
[
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
@@ -97,7 +93,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("add playlist item", () =>
{
- SelectedRoom.Value!.Playlist =
+ room.Playlist =
[
new PlaylistItem(new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo)
{
@@ -122,7 +118,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set playlist", () =>
{
- SelectedRoom.Value!.Playlist =
+ room.Playlist =
[
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
@@ -139,7 +135,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("set playlist", () =>
{
- SelectedRoom.Value!.Playlist =
+ room.Playlist =
[
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
{
@@ -170,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("add playlist item with allowed mod", () =>
{
- SelectedRoom.Value!.Playlist =
+ room.Playlist =
[
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
@@ -199,7 +195,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("add playlist item with allowed mod", () =>
{
- SelectedRoom.Value!.Playlist =
+ room.Playlist =
[
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
@@ -223,7 +219,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("add playlist item with no allowed mods", () =>
{
- SelectedRoom.Value!.Playlist =
+ room.Playlist =
[
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
@@ -246,7 +242,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("add two playlist items", () =>
{
- SelectedRoom.Value!.Playlist =
+ room.Playlist =
[
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
{
@@ -285,7 +281,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("add playlist item", () =>
{
- SelectedRoom.Value!.Playlist =
+ room.Playlist =
[
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
index 238a716f91..e7e6112297 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
@@ -25,9 +25,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiplayerParticipantsList : MultiplayerTestScene
{
- [SetUpSteps]
- public void SetupSteps()
+ public override void SetUpSteps()
{
+ base.SetUpSteps();
+
+ JoinDefaultRoom();
createNewParticipantsList();
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
index 94dd114c32..1a5be48cad 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
@@ -22,6 +22,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private MultiplayerPlayer player = null!;
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+ JoinDefaultRoom();
+ }
+
[Test]
public void TestGameplay()
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
index 77b75f407b..406c6cacae 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
@@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!;
private BeatmapInfo importedBeatmap = null!;
+ private Room room = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@@ -46,9 +47,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
base.SetUpSteps();
+ JoinDefaultRoom(r => room = r);
+
AddStep("create list", () =>
{
- Child = list = new MultiplayerPlaylist(SelectedRoom.Value!)
+ Child = list = new MultiplayerPlaylist(room)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
index 3ef2e4ecf4..5eba67bab5 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
@@ -29,6 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!;
private BeatmapInfo importedBeatmap = null!;
+ private Room room = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@@ -42,9 +43,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
base.SetUpSteps();
+ JoinDefaultRoom(r => room = r);
+
AddStep("create playlist", () =>
{
- Child = playlist = new MultiplayerQueueList(SelectedRoom.Value!)
+ Child = playlist = new MultiplayerQueueList(room)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
index 1429f86164..f92721b04b 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
@@ -28,6 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private MultiplayerSpectateButton spectateButton = null!;
private MatchStartControl startControl = null!;
+ private Room room = null!;
private BeatmapSetInfo importedSet = null!;
private BeatmapManager beatmaps = null!;
@@ -46,11 +47,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
base.SetUpSteps();
+ JoinDefaultRoom(r => room = r);
+
AddStep("create button", () =>
{
- PlaylistItem item = SelectedRoom.Value!.Playlist.First();
-
- AvailabilityTracker.SelectedItem.Value = item;
+ AvailabilityTracker.SelectedItem.Value = room.Playlist.First();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
@@ -69,14 +70,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
- SelectedItem = new Bindable(item)
+ SelectedItem = new Bindable(room.Playlist.First())
},
startControl = new MatchStartControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
- SelectedItem = new Bindable(item)
+ SelectedItem = new Bindable(room.Playlist.First())
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
index 726d0ac9f9..7c73fb8321 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs
@@ -27,6 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private BeatmapManager manager = null!;
private TestPlaylistsSongSelect songSelect = null!;
+ private Room room = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@@ -51,13 +52,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("reset", () =>
{
- SelectedRoom.Value = new Room();
+ room = new Room();
Ruleset.Value = new OsuRuleset().RulesetInfo;
Beatmap.SetDefault();
SelectedMods.Value = Array.Empty();
});
- AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect(SelectedRoom.Value!)));
+ AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect(room)));
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
}
@@ -65,14 +66,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestItemAddedIfEmptyOnStart()
{
AddStep("finalise selection", () => songSelect.FinaliseSelection());
- AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1);
+ AddAssert("playlist has 1 item", () => room.Playlist.Count == 1);
}
[Test]
public void TestItemAddedWhenCreateNewItemClicked()
{
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
- AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1);
+ AddAssert("playlist has 1 item", () => room.Playlist.Count == 1);
}
[Test]
@@ -80,7 +81,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
AddStep("finalise selection", () => songSelect.FinaliseSelection());
- AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1);
+ AddAssert("playlist has 1 item", () => room.Playlist.Count == 1);
}
[Test]
@@ -88,7 +89,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
- AddAssert("playlist has 2 items", () => SelectedRoom.Value!.Playlist.Count == 2);
+ AddAssert("playlist has 2 items", () => room.Playlist.Count == 2);
}
[Test]
@@ -96,10 +97,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
- AddStep("rearrange", () => SelectedRoom.Value!.Playlist = SelectedRoom.Value!.Playlist.Skip(1).Append(SelectedRoom.Value!.Playlist[0]).ToArray());
+ AddStep("rearrange", () => room.Playlist = room.Playlist.Skip(1).Append(room.Playlist[0]).ToArray());
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
- AddAssert("new item has id 2", () => SelectedRoom.Value!.Playlist.Last().ID == 2);
+ AddAssert("new item has id 2", () => room.Playlist.Last().ID == 2);
}
///
@@ -115,13 +116,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("item 1 has rate 1.5", () =>
{
- var mod = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset());
+ var mod = (OsuModDoubleTime)room.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset());
return Precision.AlmostEquals(1.5, mod.SpeedChange.Value);
});
AddAssert("item 2 has rate 2", () =>
{
- var mod = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.Last().RequiredMods[0].ToMod(new OsuRuleset());
+ var mod = (OsuModDoubleTime)room.Playlist.Last().RequiredMods[0].ToMod(new OsuRuleset());
return Precision.AlmostEquals(2, mod.SpeedChange.Value);
});
}
@@ -147,7 +148,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2);
AddAssert("item has rate 1.5", () =>
{
- var m = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset());
+ var m = (OsuModDoubleTime)room.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset());
return Precision.AlmostEquals(1.5, m.SpeedChange.Value);
});
}
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
index f7b0bc0d58..c714c39e22 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs
@@ -18,6 +18,7 @@ namespace osu.Game.Tests.Visual.Playlists
public partial class TestScenePlaylistsMatchSettingsOverlay : OnlinePlayTestScene
{
private TestRoomSettings settings = null!;
+ private Room room = null!;
private Func? handleRequest;
public override void SetUpSteps()
@@ -47,9 +48,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("create overlay", () =>
{
- SelectedRoom.Value = new Room();
-
- Child = settings = new TestRoomSettings(SelectedRoom.Value!)
+ Child = settings = new TestRoomSettings(room = new Room())
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible }
@@ -62,19 +61,19 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("clear name and beatmap", () =>
{
- SelectedRoom.Value!.Name = "";
- SelectedRoom.Value!.Playlist = [];
+ room.Name = "";
+ room.Playlist = [];
});
AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
- AddStep("set name", () => SelectedRoom.Value!.Name = "Room name");
+ AddStep("set name", () => room.Name = "Room name");
AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
- AddStep("set beatmap", () => SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]);
+ AddStep("set beatmap", () => room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]);
AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value);
- AddStep("clear name", () => SelectedRoom.Value!.Name = "");
+ AddStep("clear name", () => room.Name = "");
AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
}
@@ -90,7 +89,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
settings.NameField.Current.Value = expected_name;
settings.DurationField.Current.Value = expectedDuration;
- SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)];
+ room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)];
handleRequest = r =>
{
@@ -115,8 +114,8 @@ namespace osu.Game.Tests.Visual.Playlists
{
var beatmap = CreateBeatmap(Ruleset.Value).BeatmapInfo;
- SelectedRoom.Value!.Name = "Test Room";
- SelectedRoom.Value!.Playlist = [new PlaylistItem(beatmap)];
+ room.Name = "Test Room";
+ room.Playlist = [new PlaylistItem(beatmap)];
errorMessage = $"{not_found_prefix} {beatmap.OnlineID}";
@@ -124,13 +123,13 @@ namespace osu.Game.Tests.Visual.Playlists
});
AddAssert("error not displayed", () => !settings.ErrorText.IsPresent);
- AddAssert("playlist item valid", () => SelectedRoom.Value!.Playlist[0].Valid.Value);
+ AddAssert("playlist item valid", () => room.Playlist[0].Valid.Value);
AddStep("create room", () => settings.ApplyButton.Action.Invoke());
AddAssert("error displayed", () => settings.ErrorText.IsPresent);
AddAssert("error has custom text", () => settings.ErrorText.Text != errorMessage);
- AddAssert("playlist item marked invalid", () => !SelectedRoom.Value!.Playlist[0].Valid.Value);
+ AddAssert("playlist item marked invalid", () => !room.Playlist[0].Valid.Value);
}
[Test]
@@ -142,8 +141,8 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("setup", () =>
{
- SelectedRoom.Value!.Name = "Test Room";
- SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)];
+ room.Name = "Test Room";
+ room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)];
handleRequest = _ => failText;
});
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs
index c60b208ffc..e1ec30d02a 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs
@@ -14,13 +14,15 @@ namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestScenePlaylistsParticipantsList : OnlinePlayTestScene
{
+ private Room room = null!;
+
public override void SetUpSteps()
{
base.SetUpSteps();
- AddStep("create list", () =>
+ AddStep("create room", () =>
{
- SelectedRoom.Value = new Room
+ room = new Room
{
RoomID = 7,
RecentParticipants = Enumerable.Range(0, 50).Select(_ => new APIUser
@@ -38,7 +40,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("create component", () =>
{
- Child = new ParticipantsDisplay(SelectedRoom.Value!, Direction.Horizontal)
+ Child = new ParticipantsDisplay(room, Direction.Horizontal)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -52,7 +54,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("create component", () =>
{
- Child = new ParticipantsDisplay(SelectedRoom.Value!, Direction.Vertical)
+ Child = new ParticipantsDisplay(room, Direction.Vertical)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
index 0270840597..a748d61d44 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs
@@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Playlists
private BeatmapManager manager = null!;
private TestPlaylistsRoomSubScreen match = null!;
private BeatmapSetInfo importedBeatmap = null!;
+ private Room room = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@@ -47,11 +48,9 @@ namespace osu.Game.Tests.Visual.Playlists
[SetUpSteps]
public void SetupSteps()
{
- AddStep("set room", () => SelectedRoom.Value = new Room());
-
importBeatmap();
- AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom.Value!)));
+ AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(room = new Room())));
AddUntilStep("wait for load", () => match.IsCurrentScreen());
}
@@ -119,7 +118,7 @@ namespace osu.Game.Tests.Visual.Playlists
];
});
- AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value!.Playlist[0]);
+ AddAssert("first playlist item selected", () => match.SelectedItem.Value == room.Playlist[0]);
}
[Test]
@@ -197,10 +196,9 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("match has correct beatmap", () => realHash == match.Beatmap.Value.BeatmapInfo.MD5Hash);
}
- private void setupAndCreateRoom(Action room)
+ private void setupAndCreateRoom(Action setupFunc)
{
- AddStep("setup room", () => room(SelectedRoom.Value!));
-
+ AddStep("setup room", () => setupFunc(room));
AddStep("click create button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType().Single());
diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
index d1497d5142..97c213c7b1 100644
--- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.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.Online.Rooms;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual.OnlinePlay;
@@ -23,43 +24,43 @@ namespace osu.Game.Tests.Visual.Multiplayer
public bool RoomJoined => MultiplayerClient.RoomJoined;
- private readonly bool joinRoom;
-
- protected MultiplayerTestScene(bool joinRoom = true)
+ ///
+ /// Creates and joins a basic multiplayer room.
+ ///
+ /// A callback that may be used to further set up the room.
+ protected void JoinDefaultRoom(Action? setupFunc = null)
{
- this.joinRoom = joinRoom;
- }
-
- protected virtual Room CreateRoom()
- {
- return new Room
+ AddStep("join room", () =>
{
- Name = "test name",
- Type = MatchType.HeadToHead,
- Playlist =
- [
- new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
- {
- RulesetID = Ruleset.Value.OnlineID
- }
- ]
- };
- }
-
- public override void SetUpSteps()
- {
- base.SetUpSteps();
-
- if (joinRoom)
- {
- AddStep("join room", () =>
+ Room room = new Room
{
- SelectedRoom.Value = CreateRoom();
- MultiplayerClient.CreateRoom(SelectedRoom.Value).ConfigureAwait(false);
- });
+ Name = "test name",
+ Type = MatchType.HeadToHead,
+ Playlist =
+ [
+ new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
+ {
+ RulesetID = Ruleset.Value.OnlineID
+ }
+ ]
+ };
- AddUntilStep("wait for room join", () => RoomJoined);
- }
+ setupFunc?.Invoke(room);
+
+ MultiplayerClient.CreateRoom(room).ConfigureAwait(false);
+ });
+
+ AddUntilStep("wait for room join", () => RoomJoined);
+ }
+
+ ///
+ /// Creates and joins the given room.
+ ///
+ /// The room to create. If null, a default room will be created.
+ protected void JoinRoom(Room room)
+ {
+ AddStep("join room", () => MultiplayerClient.CreateRoom(room).ConfigureAwait(false));
+ AddUntilStep("wait for room join", () => RoomJoined);
}
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies();
diff --git a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs
index 5780cf6eff..60730ee9a4 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/IOnlinePlayTestSceneDependencies.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.
-using osu.Framework.Bindables;
using osu.Game.Database;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay;
@@ -13,11 +12,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay
///
public interface IOnlinePlayTestSceneDependencies
{
- ///
- /// The cached .
- ///
- Bindable SelectedRoom { get; }
-
///
/// The cached .
///
diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
index c3a5e1c3ec..ce8df36590 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
@@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
@@ -22,7 +21,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay
///
public abstract partial class OnlinePlayTestScene : ScreenTestScene, IOnlinePlayTestSceneDependencies
{
- public Bindable SelectedRoom => OnlinePlayDependencies.SelectedRoom;
public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies.OngoingOperationTracker;
public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies.AvailabilityTracker;
public TestUserLookupCache UserLookupCache => OnlinePlayDependencies.UserLookupCache;
diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs
index cc448beea0..9537c7958c 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestSceneDependencies.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Database;
using osu.Game.Online.Rooms;
@@ -18,7 +17,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay
///
public class OnlinePlayTestSceneDependencies : IReadOnlyDependencyContainer, IOnlinePlayTestSceneDependencies
{
- public Bindable SelectedRoom { get; }
public OngoingOperationTracker OngoingOperationTracker { get; }
public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; }
public TestRoomRequestsHandler RequestsHandler { get; }
@@ -35,7 +33,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay
public OnlinePlayTestSceneDependencies()
{
- SelectedRoom = new Bindable();
RequestsHandler = new TestRoomRequestsHandler();
OngoingOperationTracker = new OngoingOperationTracker();
AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
@@ -45,7 +42,6 @@ namespace osu.Game.Tests.Visual.OnlinePlay
dependencies = new DependencyContainer();
CacheAs(RequestsHandler);
- CacheAs(SelectedRoom);
CacheAs(OngoingOperationTracker);
CacheAs(AvailabilityTracker);
CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum));
From d923a478e9a044432cd611424ff57b5862d69865 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Thu, 13 Feb 2025 00:04:33 +0900
Subject: [PATCH 086/262] Remove unused method
---
.../Tests/Visual/Multiplayer/MultiplayerTestScene.cs | 10 ----------
1 file changed, 10 deletions(-)
diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
index 97c213c7b1..8150807f4f 100644
--- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
@@ -53,16 +53,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for room join", () => RoomJoined);
}
- ///
- /// Creates and joins the given room.
- ///
- /// The room to create. If null, a default room will be created.
- protected void JoinRoom(Room room)
- {
- AddStep("join room", () => MultiplayerClient.CreateRoom(room).ConfigureAwait(false));
- AddUntilStep("wait for room join", () => RoomJoined);
- }
-
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies();
}
}
From 4e66536ae8a65219c971202addb6394c6744d1fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Fri, 14 Feb 2025 15:52:05 +0100
Subject: [PATCH 087/262] Fix failed scores with no hits on beatmaps with
ridiculous mod combinations showing hundreds of pp points awarded (#31741)
See
https://discord.com/channels/188630481301012481/1097318920991559880/1334716356582572074.
On `master` this is actually worse and shows thousands of pp points, so
I guess `pp-dev` is a comparable improvement, but still flagrantly
wrong. The reason why `pp-dev` is better is the `speedDeviation == null`
guard at the start of `computeSpeedValue()` which turns off the rest of
the calculation, therefore not exposing the bug where
`relevantTotalDiff` can go negative. I still guarded it in this commit
just for safety's sake given it is clear it can do very wrong stuff.
---
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 09ec890926..a667d12a44 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= speedHighDeviationMultiplier;
// Calculate accuracy assuming the worst case scenario
- double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
+ double relevantTotalDiff = Math.Max(0, totalHits - attributes.SpeedNoteCount);
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat));
double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk));
@@ -297,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
amountHitObjectsWithAccuracy += attributes.SliderCount;
if (amountHitObjectsWithAccuracy > 0)
- betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
+ betterAccuracyPercentage = ((countGreat - Math.Max(totalHits - amountHitObjectsWithAccuracy, 0)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
else
betterAccuracyPercentage = 0;
From f868f03e1b75556418c8cfd6576781c3324d3fd7 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Fri, 21 Feb 2025 16:38:55 +0900
Subject: [PATCH 088/262] Fix host change sounds playing when exiting
multiplayer rooms
---
.../Online/Multiplayer/MultiplayerClient.cs | 6 +++++
.../Multiplayer/MultiplayerRoomSounds.cs | 27 ++++++-------------
2 files changed, 14 insertions(+), 19 deletions(-)
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 97161cce48..2d445ea25a 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -51,6 +51,11 @@ namespace osu.Game.Online.Multiplayer
///
public event Action? UserKicked;
+ ///
+ /// Invoked when the room's host is changed.
+ ///
+ public event Action? HostChanged;
+
///
/// Invoked when a new item is added to the playlist.
///
@@ -531,6 +536,7 @@ namespace osu.Game.Online.Multiplayer
Room.Host = user;
APIRoom.Host = user?.User;
+ HostChanged?.Invoke(user);
RoomUpdated?.Invoke();
}, false);
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs
index d53e485c86..cdf4e96bad 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.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.
-using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -20,7 +19,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private Sample? userJoinedSample;
private Sample? userLeftSample;
private Sample? userKickedSample;
- private MultiplayerRoomUser? host;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
@@ -35,25 +33,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.LoadComplete();
- client.RoomUpdated += onRoomUpdated;
client.UserJoined += onUserJoined;
client.UserLeft += onUserLeft;
client.UserKicked += onUserKicked;
- updateState();
- }
-
- private void onRoomUpdated() => Scheduler.AddOnce(updateState);
-
- private void updateState()
- {
- if (EqualityComparer.Default.Equals(host, client.Room?.Host))
- return;
-
- // only play sound when the host changes from an already-existing host.
- if (host != null)
- Scheduler.AddOnce(() => hostChangedSample?.Play());
-
- host = client.Room?.Host;
+ client.HostChanged += onHostChanged;
}
private void onUserJoined(MultiplayerRoomUser user)
@@ -65,16 +48,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void onUserKicked(MultiplayerRoomUser user)
=> Scheduler.AddOnce(() => userKickedSample?.Play());
+ private void onHostChanged(MultiplayerRoomUser? host)
+ {
+ if (host != null)
+ Scheduler.AddOnce(() => hostChangedSample?.Play());
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client.IsNotNull())
{
- client.RoomUpdated -= onRoomUpdated;
client.UserJoined -= onUserJoined;
client.UserLeft -= onUserLeft;
client.UserKicked -= onUserKicked;
+ client.HostChanged -= onHostChanged;
}
}
}
From fa49b30b5cc077f807f60fd6964bf5416f5ec845 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Fri, 21 Feb 2025 11:30:52 +0100
Subject: [PATCH 089/262] Attempt to fix spectator list showing other users in
multiplayer room even if they're not spectating better
Maybe closes https://github.com/ppy/osu/issues/31972. Not sure. I have
no reproduction scenario to work with, no solid understanding of how
the issue can happen, and if this doesn't fix it, then I'm not even
entirely sure how this can ever be fixed client-side.
The working theory is that not watching updates to the room provoked a
situation wherein the room was temporarily not in a correct state when
`WatchingUsers` changed, therefore the collection change callback failed
to exclude other players in the room from display.
I'm only PRing this because of the `next-release` tag on the issue.
---
osu.Game/Screens/Play/HUD/SpectatorList.cs | 80 ++++++++++++++++------
1 file changed, 60 insertions(+), 20 deletions(-)
diff --git a/osu.Game/Screens/Play/HUD/SpectatorList.cs b/osu.Game/Screens/Play/HUD/SpectatorList.cs
index 4297c62712..98b3ede874 100644
--- a/osu.Game/Screens/Play/HUD/SpectatorList.cs
+++ b/osu.Game/Screens/Play/HUD/SpectatorList.cs
@@ -2,13 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -38,8 +38,9 @@ namespace osu.Game.Screens.Play.HUD
public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White);
private BindableList watchingUsers { get; } = new BindableList();
+ private BindableList actualSpectators { get; } = new BindableList();
+
private Bindable userPlayingState { get; } = new Bindable();
- private int displayedSpectatorCount;
private OsuSpriteText header = null!;
private FillFlowContainer mainFlow = null!;
@@ -94,7 +95,9 @@ namespace osu.Game.Screens.Play.HUD
((IBindableList)watchingUsers).BindTo(client.WatchingUsers);
((IBindable)userPlayingState).BindTo(gameplayState.PlayingState);
- watchingUsers.BindCollectionChanged(onSpectatorsChanged, true);
+ watchingUsers.BindCollectionChanged(onWatchingUsersChanged, true);
+ multiplayerClient.RoomUpdated += removePlayersFromMultiplayerRoom;
+ actualSpectators.BindCollectionChanged(onSpectatorsChanged, true);
userPlayingState.BindValueChanged(_ => updateVisibility());
Font.BindValueChanged(_ => updateAppearance());
@@ -104,22 +107,55 @@ namespace osu.Game.Screens.Play.HUD
this.FadeInFromZero(200, Easing.OutQuint);
}
- private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ private void onWatchingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ {
+ for (int i = 0; i < e.NewItems!.Count; i++)
+ actualSpectators.Add((SpectatorUser)e.NewItems![i]!);
+
+ break;
+ }
+
+ case NotifyCollectionChangedAction.Remove:
+ {
+ for (int i = 0; i < e.OldItems!.Count; i++)
+ actualSpectators.Remove((SpectatorUser)e.OldItems![i]!);
+
+ break;
+ }
+
+ case NotifyCollectionChangedAction.Reset:
+ {
+ actualSpectators.Clear();
+ break;
+ }
+
+ default:
+ throw new NotSupportedException();
+ }
+
+ removePlayersFromMultiplayerRoom();
+ }
+
+ private void removePlayersFromMultiplayerRoom()
+ {
+ if (multiplayerClient.Room == null)
+ return;
+
// the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores.
// this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`.
//
// we do not generally wish to display other players in the room as spectators due to that implementation detail,
// therefore this code is intended to filter out those players on the client side.
- //
- // note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions
- // (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls).
- // the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either)
- // is a lot more difficult to write correctly, given that we also rely on `BindableList`'s collection changed event arguments to properly animate this component.
- var excludedUserIds = new HashSet();
- if (multiplayerClient.Room != null)
- excludedUserIds.UnionWith(multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID));
+ var excludedUserIds = multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID).ToHashSet();
+ actualSpectators.RemoveAll(s => excludedUserIds.Contains(s.OnlineID));
+ }
+ private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
@@ -129,9 +165,6 @@ namespace osu.Game.Screens.Play.HUD
var spectator = (SpectatorUser)e.NewItems![i]!;
int index = Math.Max(e.NewStartingIndex, 0) + i;
- if (excludedUserIds.Contains(spectator.OnlineID))
- continue;
-
if (index >= max_spectators_displayed)
break;
@@ -148,10 +181,10 @@ namespace osu.Game.Screens.Play.HUD
for (int i = 0; i < spectatorsFlow.Count; i++)
spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i);
- if (watchingUsers.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed)
+ if (actualSpectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed)
{
for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++)
- addNewSpectatorToList(i, watchingUsers[i]);
+ addNewSpectatorToList(i, actualSpectators[i]);
}
break;
@@ -167,8 +200,7 @@ namespace osu.Game.Screens.Play.HUD
throw new NotSupportedException();
}
- displayedSpectatorCount = watchingUsers.Count(s => !excludedUserIds.Contains(s.OnlineID));
- header.Text = SpectatorListStrings.SpectatorCount(displayedSpectatorCount).ToUpper();
+ header.Text = SpectatorListStrings.SpectatorCount(actualSpectators.Count).ToUpper();
updateVisibility();
for (int i = 0; i < spectatorsFlow.Count; i++)
@@ -193,7 +225,7 @@ namespace osu.Game.Screens.Play.HUD
private void updateVisibility()
{
// We don't want to show spectators when we are watching a replay.
- mainFlow.FadeTo(displayedSpectatorCount > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint);
+ mainFlow.FadeTo(actualSpectators.Count > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint);
}
private void updateAppearance()
@@ -204,6 +236,14 @@ namespace osu.Game.Screens.Play.HUD
Width = header.DrawWidth;
}
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (multiplayerClient.IsNotNull())
+ multiplayerClient.RoomUpdated -= removePlayersFromMultiplayerRoom;
+ }
+
private partial class SpectatorListEntry : PoolableDrawable
{
public Bindable Current { get; } = new Bindable();
From a690b0bae993f06edc45fabc6ea2b5153219cc1b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Fri, 21 Feb 2025 12:05:23 +0100
Subject: [PATCH 090/262] Adjust rounding tolerance in distance snap grid ring
colour logic
---
osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
index 88e28df8e3..8322c67def 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
@@ -159,7 +159,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// in case 2, we want *flooring* to occur, to prevent a possible off-by-one
// because of the rounding snapping forward by a chunk of time significantly too high to be considered a rounding error.
// the tolerance margin chosen here is arbitrary and can be adjusted if more cases of this are found.
- if (Precision.DefinitelyBigger(beatIndex, fractionalBeatIndex, 0.005))
+ if (Precision.DefinitelyBigger(beatIndex, fractionalBeatIndex, 0.01))
beatIndex = (int)Math.Floor(fractionalBeatIndex);
var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours);
From de78518fea14b4c12c5a2db4bc74d65335f05521 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Fri, 21 Feb 2025 12:52:59 +0100
Subject: [PATCH 091/262] Fix "use current distance snap" button incorrectly
factoring in last object with velocity
Closes https://github.com/ppy/osu/issues/32003.
---
osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs | 6 +++++-
osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs | 8 ++++----
2 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs
index 3c0889d027..45ce3206d2 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapProvider.cs
@@ -1,10 +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.Linq;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
@@ -14,7 +16,9 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
{
- float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime);
+ var lastObjectWithVelocity = EditorBeatmap.HitObjects.TakeWhile(ho => ho != after).OfType().LastOrDefault();
+
+ float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime, lastObjectWithVelocity);
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
return actualDistance / expectedDistance;
diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs
index d0b279f201..4129a6fb2c 100644
--- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs
+++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Edit
private EditorClock editorClock { get; set; } = null!;
[Resolved]
- private EditorBeatmap editorBeatmap { get; set; } = null!;
+ protected EditorBeatmap EditorBeatmap { get; private set; } = null!;
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; } = null!;
@@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Edit
}
});
- DistanceSpacingMultiplier.Value = editorBeatmap.DistanceSpacing;
+ DistanceSpacingMultiplier.Value = EditorBeatmap.DistanceSpacing;
DistanceSpacingMultiplier.BindValueChanged(multiplier =>
{
distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})";
@@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Edit
if (multiplier.NewValue != multiplier.OldValue)
onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier));
- editorBeatmap.DistanceSpacing = multiplier.NewValue;
+ EditorBeatmap.DistanceSpacing = multiplier.NewValue;
}, true);
DistanceSpacingMultiplier.BindDisabledChanged(disabled => distanceSpacingSlider.Alpha = disabled ? 0 : 1, true);
@@ -267,7 +267,7 @@ namespace osu.Game.Rulesets.Edit
public virtual float GetBeatSnapDistance(IHasSliderVelocity? withVelocity = null)
{
- return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * editorBeatmap.Difficulty.SliderMultiplier * 1
+ return (float)(100 * (withVelocity?.SliderVelocityMultiplier ?? 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1
/ beatSnapProvider.BeatDivisor);
}
From 8b2582a69d07adf343855b729dd143777abbcbf6 Mon Sep 17 00:00:00 2001
From: finadoggie <75299710+Finadoggie@users.noreply.github.com>
Date: Sat, 22 Feb 2025 16:54:27 -0800
Subject: [PATCH 092/262] Add tip pressure threshold slider ingame
---
.../Settings/Sections/Input/TabletSettings.cs | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 00ffbc1120..2cce6f18ec 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -45,6 +45,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private readonly BindableNumber rotation = new BindableNumber { MinValue = 0, MaxValue = 360 };
+ private readonly BindableNumber pressureThreshold = new BindableNumber { MinValue = 0, MaxValue = 100 };
+
[Resolved]
private GameHost host { get; set; }
@@ -213,6 +215,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input
Current = sizeY,
CanBeShown = { BindTarget = enabled }
},
+ new SettingsSlider
+ {
+ TransferValueOnCommit = true,
+ LabelText = "Tip Threshold",
+ Current = pressureThreshold,
+ CanBeShown = { BindTarget = enabled }
+ },
}
},
};
@@ -267,6 +276,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
aspectRatioApplication = Schedule(() => forceAspectRatio(aspect.NewValue));
});
+ pressureThreshold.BindTo(tabletHandler.PressureThreshold);
+
tablet.BindTo(tabletHandler.Tablet);
tablet.BindValueChanged(val => Schedule(() =>
{
From 543ad5b2a47591652d04ac66eb8730cafd7e06b9 Mon Sep 17 00:00:00 2001
From: Kunologist <2014709936@qq.com>
Date: Mon, 24 Feb 2025 14:16:33 +0800
Subject: [PATCH 093/262] Add alt+wheel volume adjustment on result screen
---
osu.Game/Screens/Ranking/ResultsScreen.cs | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index fe0d805cee..8fb3c66054 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -26,6 +26,7 @@ using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Placeholders;
using osu.Game.Overlays;
+using osu.Game.Overlays.Volume;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking.Expanded.Accuracy;
@@ -122,6 +123,7 @@ namespace osu.Game.Screens.Ranking
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
+ new GlobalScrollAdjustsVolume(),
StatisticsPanel = createStatisticsPanel().With(panel =>
{
panel.RelativeSizeAxes = Axes.Both;
@@ -503,12 +505,24 @@ namespace osu.Game.Screens.Ranking
{
}
+ protected override bool OnScroll(ScrollEvent e)
+ {
+ // Match stable behaviour of only alt-scroll adjusting volume.
+ // This is the same behaviour as the song selection screen.
+ if (!e.CurrentState.Keyboard.AltPressed)
+ return true;
+
+ return base.OnScroll(e);
+ }
+
protected partial class VerticalScrollContainer : OsuScrollContainer
{
protected override Container Content => content;
private readonly Container content;
+ protected override bool OnScroll(ScrollEvent e) => !e.ControlPressed && !e.AltPressed && !e.ShiftPressed && !e.SuperPressed;
+
public VerticalScrollContainer()
{
Masking = false;
From f4b427ee66bd169ab7270f9a4ef8f467d4ac4572 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Mon, 24 Feb 2025 09:15:20 +0100
Subject: [PATCH 094/262] Add failing test case
---
.../TestSceneModDifficultyAdjustSettings.cs | 24 +++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
index b40d0b10d2..30470c9c17 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
@@ -18,6 +18,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osuTK;
using osuTK.Graphics;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
@@ -220,6 +221,29 @@ namespace osu.Game.Tests.Visual.UserInterface
checkBindableAtValue("Circle Size", null);
}
+ [Test]
+ public void TestResetToDefaultViaDoubleClickingNub()
+ {
+ setBeatmapWithDifficultyParameters(5);
+
+ setSliderValue("Circle Size", 3);
+ setExtendedLimits(true);
+
+ checkSliderAtValue("Circle Size", 3);
+ checkBindableAtValue("Circle Size", 3);
+
+ AddStep("double click circle size nub", () =>
+ {
+ var nub = this.ChildrenOfType.SliderNub>().First();
+ InputManager.MoveMouseTo(nub);
+ InputManager.Click(MouseButton.Left);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ checkSliderAtValue("Circle Size", 5);
+ checkBindableAtValue("Circle Size", null);
+ }
+
[Test]
public void TestModSettingChangeTracker()
{
From d8cb3b68d3ea90268bb142a2ef6e1de782f2040a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Mon, 24 Feb 2025 09:34:52 +0100
Subject: [PATCH 095/262] Add "Team" channel type
The lack of this bricks chat completely due to newtonsoft
deserialisation errors:
2025-02-24 08:32:58 [verbose]: Processing response from https://dev.ppy.sh/api/v2/chat/updates failed with Newtonsoft.Json.JsonSerializationException: Error converting value "TEAM" to type 'osu.Game.Online.Chat.ChannelType'. Path 'presence[39].type', line 1, position 13765.
2025-02-24 08:32:58 [verbose]: ---> System.ArgumentException: Requested value 'TEAM' was not found.
2025-02-24 08:32:58 [verbose]: at Newtonsoft.Json.Utilities.EnumUtils.ParseEnum(Type enumType, NamingStrategy namingStrategy, String value, Boolean disallowNumber)
2025-02-24 08:32:58 [verbose]: at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.EnsureType(JsonReader reader, Object value, CultureInfo culture, JsonContract contract, Type targetType)
---
osu.Game/Online/Chat/ChannelType.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/osu.Game/Online/Chat/ChannelType.cs b/osu.Game/Online/Chat/ChannelType.cs
index bd628e90c4..4fb890c2cc 100644
--- a/osu.Game/Online/Chat/ChannelType.cs
+++ b/osu.Game/Online/Chat/ChannelType.cs
@@ -14,5 +14,6 @@ namespace osu.Game.Online.Chat
Group,
System,
Announce,
+ Team,
}
}
From be8ec759488e3bb5e5479341c8de400a80f3e9ec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Mon, 24 Feb 2025 09:39:27 +0100
Subject: [PATCH 096/262] Display team chat channel in separate group
---
osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
index f027888962..6e874e4ed8 100644
--- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
+++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
@@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
public ChannelGroup AnnounceChannelGroup { get; private set; } = null!;
public ChannelGroup PublicChannelGroup { get; private set; } = null!;
+ public ChannelGroup TeamChannelGroup { get; private set; } = null!;
public ChannelGroup PrivateChannelGroup { get; private set; } = null!;
private OsuScrollContainer scroll = null!;
@@ -82,6 +83,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false),
PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false),
selector = new ChannelListItem(ChannelListingChannel),
+ TeamChannelGroup = new ChannelGroup("TEAM", false), // TODO: replace with osu-web localisable string once available
PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true),
},
},
@@ -156,6 +158,9 @@ namespace osu.Game.Overlays.Chat.ChannelList
case ChannelType.Announce:
return AnnounceChannelGroup;
+ case ChannelType.Team:
+ return TeamChannelGroup;
+
default:
return PublicChannelGroup;
}
From 194f05d2588fa7f23287b85c3968219102e33a51 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Mon, 24 Feb 2025 09:43:46 +0100
Subject: [PATCH 097/262] Add icons to chat channel group headers
Matches web in appearance.
Cross-reference: https://github.com/ppy/osu-web/blob/3c9e99eaf4bd9e73d2712f60d67f5bc95f9dfe2b/resources/js/chat/conversation-list.tsx#L13-L19
---
.../Overlays/Chat/ChannelList/ChannelList.cs | 35 ++++++++++++++-----
1 file changed, 26 insertions(+), 9 deletions(-)
diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
index 6e874e4ed8..ae68c9c82e 100644
--- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
+++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
@@ -9,6 +9,7 @@ using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Testing;
@@ -80,11 +81,12 @@ namespace osu.Game.Overlays.Chat.ChannelList
RelativeSizeAxes = Axes.X,
}
},
- AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), false),
- PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), false),
+ // cross-reference for icons: https://github.com/ppy/osu-web/blob/3c9e99eaf4bd9e73d2712f60d67f5bc95f9dfe2b/resources/js/chat/conversation-list.tsx#L13-L19
+ AnnounceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper(), FontAwesome.Solid.Bullhorn, false),
+ PublicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper(), FontAwesome.Solid.Comments, false),
selector = new ChannelListItem(ChannelListingChannel),
- TeamChannelGroup = new ChannelGroup("TEAM", false), // TODO: replace with osu-web localisable string once available
- PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), true),
+ TeamChannelGroup = new ChannelGroup("TEAM", FontAwesome.Solid.Users, false), // TODO: replace with osu-web localisable string once available
+ PrivateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper(), FontAwesome.Solid.Envelope, true),
},
},
},
@@ -179,7 +181,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
private readonly bool sortByRecent;
public readonly ChannelListItemFlow ItemFlow;
- public ChannelGroup(LocalisableString label, bool sortByRecent)
+ public ChannelGroup(LocalisableString label, IconUsage icon, bool sortByRecent)
{
this.sortByRecent = sortByRecent;
Direction = FillDirection.Vertical;
@@ -189,11 +191,26 @@ namespace osu.Game.Overlays.Chat.ChannelList
Children = new Drawable[]
{
- new OsuSpriteText
+ new FillFlowContainer
{
- Text = label,
- Margin = new MarginPadding { Left = 18, Bottom = 5 },
- Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(5),
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Text = label,
+ Margin = new MarginPadding { Left = 18, Bottom = 5 },
+ Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
+ },
+ new SpriteIcon
+ {
+ Icon = icon,
+ Size = new Vector2(12),
+ },
+ }
},
ItemFlow = new ChannelListItemFlow(sortByRecent)
{
From 4ac4b308e10d041dec5960f808ce2d295171f3d2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Mon, 24 Feb 2025 09:48:03 +0100
Subject: [PATCH 098/262] Add visual test coverage of team channels
---
.../Visual/Online/TestSceneChannelList.cs | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs
index 5f77e084da..8f8cf036f1 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs
@@ -115,6 +115,12 @@ namespace osu.Game.Tests.Visual.Online
channelList.AddChannel(createRandomPrivateChannel());
});
+ AddStep("Add Team Channels", () =>
+ {
+ for (int i = 0; i < 10; i++)
+ channelList.AddChannel(createRandomTeamChannel());
+ });
+
AddStep("Add Announce Channels", () =>
{
for (int i = 0; i < 2; i++)
@@ -189,5 +195,16 @@ namespace osu.Game.Tests.Visual.Online
Id = id,
};
}
+
+ private Channel createRandomTeamChannel()
+ {
+ int id = TestResources.GetNextTestID();
+ return new Channel
+ {
+ Name = $"Team {id}",
+ Type = ChannelType.Team,
+ Id = id,
+ };
+ }
}
}
From 0312467c8840067054c6326d9da821329b7bf01e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Mon, 24 Feb 2025 12:30:37 +0100
Subject: [PATCH 099/262] Fix hash comparison being case sensitive when
choosing files for partial beatmap submission
Noticed when investigating https://github.com/ppy/osu/issues/32059, and
also a likely cause for user reports like
https://discord.com/channels/188630481301012481/1097318920991559880/1342962553101357066.
Honestly I have no solid defence, Your Honour. I guess this just must
not have been tested on the client side, only relied on server-side
testing.
---
osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs
index 66139bacec..13981bcb69 100644
--- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs
+++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs
@@ -285,7 +285,7 @@ namespace osu.Game.Screens.Edit.Submission
continue;
}
- if (localHash != onlineHash)
+ if (!localHash.Equals(onlineHash, StringComparison.OrdinalIgnoreCase))
filesToUpdate.Add(filename);
}
From 41db3c1501bbfc40f1eb9952fa9d332319ff347b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Mon, 24 Feb 2025 14:30:55 +0100
Subject: [PATCH 100/262] Fix taiko swell ending samples playing at results
sometimes
Closes https://github.com/ppy/osu/issues/32052.
Sooooo... this is going to be a rant...
To understand why this is going to require a rant, dear reader, please
do the following:
1. Read the issue thread and follow the reproduction scenario (download
map linked, fire up autoplay, seek near end, wait for results, hear
the sample spam).
2. Now exit out to song select, *hide the toolbar*, and attempt
reproducing the issue again.
3. Depending on ambient mood, laugh or cry.
Now, *why on earth* would the *TOOLBAR* have any bearing on anything?
Well, the chain of failure is something like this:
- The toolbar hides for the duration of gameplay, naturally.
- When progressing to results, the toolbar gets automatically unhidden.
- This triggers invalidations on `ScrollingHitObjectContainer`. I'm not
precisely sure which property it is that triggers the invalidations,
but one clearly does. It may be position or size or whichever.
- When the invalidation is triggered on `layoutCache`, the next
`Update()` call is going to recompute lifetimes for ALL hitobject
entries.
- In case of swells, it happens that the calculated lifetime end of the
swell is larger than what it actually ended up being determined as at
the instant of judging the swell, and thus, the swell is *resurrected*,
reassigned a DHO, and the DHO calls `UpdateState()` and plays the
sample again despite the `samplePlayed` flag in `LegacySwell`, because
all of that is ephemeral state that does not survive a hitobject
getting resurrected.
Now I *could* just fix this locally to the swell, maybe, by having some
time lenience check, but the fact that hitobjects can be resurrected by
the *toolbar* appearing, of all possible causes in the world, feels
just completely wrong. So I'm adding a local check in SHOC to not
overwrite lifetime ends of judged object entries.
The reason why I'm making that check specific to end time is that I can
see valid reasons why you would want to recompute lifetime *start* even
on a judged object (such as playfield geometry changing in a significant
way). I can't think of a valid reason to do that to lifetime *end*.
---
.../Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 7841e65935..8b0076afa1 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -247,7 +247,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
// It is required that we set a lifetime end here to ensure that in scenarios like loading a Player instance to a seeked
// location in a beatmap doesn't churn every hit object into a DrawableHitObject. Even in a pooled scenario, the overhead
// of this can be quite crippling.
- entry.LifetimeEnd = entry.HitObject.GetEndTime() + timeRange.Value;
+ //
+ // However, additionally do not attempt to alter lifetime of judged entries.
+ // This is to prevent freak accidents like objects suddenly becoming alive because of this estimate assigning a later lifetime
+ // than the object itself decided it should have when it underwent judgement.
+ if (!entry.Judged)
+ entry.LifetimeEnd = entry.HitObject.GetEndTime() + timeRange.Value;
}
private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null)
From e8f7bcb6e625a6360b2bd4487186ac075e07ddc2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Mon, 24 Feb 2025 15:06:02 +0100
Subject: [PATCH 101/262] Only show team channel section when there is a team
channel
---
osu.Game.Tests/Visual/Online/TestSceneChannelList.cs | 6 +-----
osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 7 +++----
2 files changed, 4 insertions(+), 9 deletions(-)
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs
index 8f8cf036f1..364240502a 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs
@@ -115,11 +115,7 @@ namespace osu.Game.Tests.Visual.Online
channelList.AddChannel(createRandomPrivateChannel());
});
- AddStep("Add Team Channels", () =>
- {
- for (int i = 0; i < 10; i++)
- channelList.AddChannel(createRandomTeamChannel());
- });
+ AddStep("Add Team Channel", () => channelList.AddChannel(createRandomTeamChannel()));
AddStep("Add Announce Channels", () =>
{
diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
index ae68c9c82e..c0fc349c2c 100644
--- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
+++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
@@ -106,6 +106,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
};
selector.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
+ updateVisibility();
}
public void AddChannel(Channel channel)
@@ -170,10 +171,8 @@ namespace osu.Game.Overlays.Chat.ChannelList
private void updateVisibility()
{
- if (AnnounceChannelGroup.ItemFlow.Children.Count == 0)
- AnnounceChannelGroup.Hide();
- else
- AnnounceChannelGroup.Show();
+ AnnounceChannelGroup.Alpha = AnnounceChannelGroup.ItemFlow.Any() ? 1 : 0;
+ TeamChannelGroup.Alpha = TeamChannelGroup.ItemFlow.Any() ? 1 : 0;
}
public partial class ChannelGroup : FillFlowContainer
From e13aa4a99b353f994e9bcf6c6df58d18f466bf66 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Mon, 24 Feb 2025 15:10:20 +0100
Subject: [PATCH 102/262] Do not allow leaving team channels
---
osu.Game/Overlays/Chat/ChannelList/ChannelList.cs | 8 ++++++--
osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs | 4 +++-
2 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
index c0fc349c2c..0a89775cc7 100644
--- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
+++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs
@@ -114,9 +114,13 @@ namespace osu.Game.Overlays.Chat.ChannelList
if (channelMap.ContainsKey(channel))
return;
- ChannelListItem item = new ChannelListItem(channel);
+ ChannelListItem item = new ChannelListItem(channel)
+ {
+ CanLeave = channel.Type != ChannelType.Team
+ };
item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
- item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan);
+ if (item.CanLeave)
+ item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan);
ChannelGroup group = getGroupFromChannel(channel);
channelMap.Add(channel, item);
diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs
index b197fe199d..6107f130ec 100644
--- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs
+++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs
@@ -24,6 +24,8 @@ namespace osu.Game.Overlays.Chat.ChannelList
public partial class ChannelListItem : OsuClickableContainer, IFilterable
{
public event Action? OnRequestSelect;
+
+ public bool CanLeave { get; init; } = true;
public event Action? OnRequestLeave;
public readonly Channel Channel;
@@ -160,7 +162,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
private ChannelListItemCloseButton? createCloseButton()
{
- if (isSelector)
+ if (isSelector || !CanLeave)
return null;
return new ChannelListItemCloseButton
From c82cf4092879167f60bd76729730350d882c248f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Mon, 24 Feb 2025 15:24:18 +0100
Subject: [PATCH 103/262] Do not give swell ticks any visual representation
Why is this a thing at all? How has it survived this long? I don't know.
As far as I can tell this only manifests on selected beatmaps with "slow
swells" that spend the entire beatmap moving in the background. On other
beatmaps the tick is faded out, probably due to the initial transform
application that normally "works" but fails hard on these slow swells.
Can be seen on https://osu.ppy.sh/beatmapsets/1432454#taiko/2948222.
---
.../Objects/Drawables/DrawableSwellTick.cs | 7 +------
.../Objects/Drawables/DrawableTaikoHitObject.cs | 6 +++++-
2 files changed, 6 insertions(+), 7 deletions(-)
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
index 04dd01e066..88554ba257 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
@@ -4,9 +4,7 @@
#nullable disable
using JetBrains.Annotations;
-using osu.Framework.Graphics;
using osu.Framework.Input.Events;
-using osu.Game.Rulesets.Taiko.Skinning.Default;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
@@ -25,8 +23,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
}
- protected override void UpdateInitialTransforms() => this.FadeOut();
-
public void TriggerResult(bool hit)
{
HitObject.StartTime = Time.Current;
@@ -43,7 +39,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public override bool OnPressed(KeyBindingPressEvent e) => false;
- protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollTick),
- _ => new TickPiece());
+ protected override SkinnableDrawable CreateMainPiece() => null;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index 0cf9651965..520ac2ba80 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -154,9 +154,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (MainPiece != null)
Content.Remove(MainPiece, true);
- Content.Add(MainPiece = CreateMainPiece());
+ MainPiece = CreateMainPiece();
+
+ if (MainPiece != null)
+ Content.Add(MainPiece);
}
+ [CanBeNull]
protected abstract SkinnableDrawable CreateMainPiece();
}
}
From 1f562ab47d5f6959f66e0091ec82b778c3539f7b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Mon, 24 Feb 2025 09:18:19 +0100
Subject: [PATCH 104/262] Fix double-clicking difficulty adjust sliders not
resetting the value to default correctly
- Closes https://github.com/ppy/osu/issues/31888
- Supersedes / closes https://github.com/ppy/osu/pull/32060
---
.../Mods/OsuModDifficultyAdjust.cs | 9 +-------
.../UserInterface/RoundedSliderBar.cs | 18 +++++++++++----
.../Mods/DifficultyAdjustSettingsControl.cs | 23 +++++++++++++------
3 files changed, 30 insertions(+), 20 deletions(-)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
index f35b1abc42..10282ff988 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
@@ -3,7 +3,6 @@
using System.Linq;
using osu.Framework.Bindables;
-using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -63,13 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private partial class ApproachRateSettingsControl : DifficultyAdjustSettingsControl
{
- protected override RoundedSliderBar CreateSlider(BindableNumber current) =>
- new ApproachRateSlider
- {
- RelativeSizeAxes = Axes.X,
- Current = current,
- KeyboardStep = 0.1f,
- };
+ protected override RoundedSliderBar CreateSlider(BindableNumber current) => new ApproachRateSlider();
///
/// A slider bar with more detailed approach rate info for its given value
diff --git a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs
index aeab7c34b2..9a0183da64 100644
--- a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Overlays;
using Vector2 = osuTK.Vector2;
@@ -52,10 +53,21 @@ namespace osu.Game.Graphics.UserInterface
}
}
+ ///
+ /// The action to use to reset the value of to the default.
+ /// Triggered on double click.
+ ///
+ public Action ResetToDefault { get; internal set; }
+
public RoundedSliderBar()
{
Height = Nub.HEIGHT;
RangePadding = Nub.DEFAULT_EXPANDED_SIZE / 2;
+ ResetToDefault = () =>
+ {
+ if (!Current.Disabled)
+ Current.SetDefault();
+ };
Children = new Drawable[]
{
new Container
@@ -102,11 +114,7 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
Current = { Value = true },
- OnDoubleClicked = () =>
- {
- if (!Current.Disabled)
- Current.SetDefault();
- },
+ OnDoubleClicked = () => ResetToDefault.Invoke(),
},
},
hoverClickSounds = new HoverClickSounds()
diff --git a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
index d04d7636ec..6697a8d848 100644
--- a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
+++ b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
@@ -31,12 +31,7 @@ namespace osu.Game.Rulesets.Mods
protected sealed override Drawable CreateControl() => new SliderControl(sliderDisplayCurrent, CreateSlider);
- protected virtual RoundedSliderBar CreateSlider(BindableNumber current) => new RoundedSliderBar
- {
- RelativeSizeAxes = Axes.X,
- Current = current,
- KeyboardStep = 0.1f,
- };
+ protected virtual RoundedSliderBar CreateSlider(BindableNumber current) => new RoundedSliderBar();
///
/// Guards against beatmap values displayed on slider bars being transferred to user override.
@@ -111,7 +106,21 @@ namespace osu.Game.Rulesets.Mods
{
InternalChildren = new Drawable[]
{
- createSlider(currentNumber)
+ createSlider(currentNumber).With(slider =>
+ {
+ slider.RelativeSizeAxes = Axes.X;
+ slider.Current = currentNumber;
+ slider.KeyboardStep = 0.1f;
+ // this looks redundant, but isn't because of the various games this component plays
+ // (`Current` is nullable and represents the underlying setting value,
+ // `currentNumber` is not nullable and represents what is getting displayed,
+ // therefore without this, double-clicking the slider would reset `currentNumber` to its bogus default of 0).
+ slider.ResetToDefault = () =>
+ {
+ if (!Current.Disabled)
+ Current.SetDefault();
+ };
+ })
};
AutoSizeAxes = Axes.Y;
From fc2d8bfe5f3b4ed3d1a0f7652dd84601e8115b75 Mon Sep 17 00:00:00 2001
From: finadoggie <75299710+Finadoggie@users.noreply.github.com>
Date: Tue, 25 Feb 2025 00:25:51 -0800
Subject: [PATCH 105/262] Clamp slider from 0 to 1
---
osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 2cce6f18ec..9d70e49659 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private readonly BindableNumber rotation = new BindableNumber { MinValue = 0, MaxValue = 360 };
- private readonly BindableNumber pressureThreshold = new BindableNumber { MinValue = 0, MaxValue = 100 };
+ private readonly BindableNumber pressureThreshold = new BindableNumber { MinValue = 0.0f, MaxValue = 1.0f, Precision = 0.005f };
[Resolved]
private GameHost host { get; set; }
From e97c2fee0d2a57d2d13c2e20a76370daa325cd4c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Tue, 25 Feb 2025 12:57:38 +0100
Subject: [PATCH 106/262] Update framework
---
osu.Android.props | 2 +-
osu.Game/osu.Game.csproj | 2 +-
osu.iOS.props | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/osu.Android.props b/osu.Android.props
index d49acd7b27..d4b49e492a 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -10,7 +10,7 @@
true
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 5ca49e80f6..d10a3d649a 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -17,6 +17,6 @@
-all
-
+
From 13ca8c20f6fa71bd196e30a5987cb112cbc7214f Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Tue, 25 Feb 2025 21:54:13 +0900
Subject: [PATCH 107/262] Make results screens use tasks to fetch scores
---
.../Visual/Ranking/TestSceneResultsScreen.cs | 17 +--
.../Spectate/MultiSpectatorResultsScreen.cs | 7 +-
.../Playlists/PlaylistItemResultsScreen.cs | 112 ++++++++++--------
.../PlaylistItemScoreResultsScreen.cs | 5 +-
.../PlaylistItemUserBestResultsScreen.cs | 5 +-
osu.Game/Screens/Ranking/ResultsScreen.cs | 51 +++-----
osu.Game/Screens/Ranking/SoloResultsScreen.cs | 31 +++--
7 files changed, 117 insertions(+), 111 deletions(-)
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index 3a08756090..4acbdb4a76 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -17,7 +17,6 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu;
@@ -416,7 +415,7 @@ namespace osu.Game.Tests.Visual.Ranking
RetryOverlay = InternalChildren.OfType().SingleOrDefault();
}
- protected override APIRequest FetchScores(Action> scoresCallback)
+ protected override Task> FetchScores()
{
var scores = new List();
@@ -428,9 +427,7 @@ namespace osu.Game.Tests.Visual.Ranking
scores.Add(score);
}
- scoresCallback.Invoke(scores);
-
- return null;
+ return Task.FromResult>(scores);
}
}
@@ -446,9 +443,9 @@ namespace osu.Game.Tests.Visual.Ranking
this.fetchWaitTask = fetchWaitTask ?? Task.CompletedTask;
}
- protected override APIRequest FetchScores(Action> scoresCallback)
+ protected override Task> FetchScores()
{
- Task.Run(async () =>
+ return Task.Run>(async () =>
{
await fetchWaitTask;
@@ -461,12 +458,10 @@ namespace osu.Game.Tests.Visual.Ranking
scores.Add(score);
}
- scoresCallback?.Invoke(scores);
-
Schedule(() => FetchCompleted = true);
- });
- return null;
+ return scores;
+ });
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs
index c240bbea0c..6e2f90e3b5 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs
@@ -1,9 +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 osu.Game.Online.API;
+using System.Threading.Tasks;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
@@ -23,8 +22,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
Scheduler.AddDelayed(() => StatisticsPanel.ToggleVisibility(), 1000);
}
- protected override APIRequest? FetchScores(Action> scoresCallback) => null;
+ protected override Task> FetchScores() => Task.FromResult>([]);
- protected override APIRequest? FetchNextPage(int direction, Action> scoresCallback) => null;
+ protected override Task> FetchNextPage(int direction) => Task.FromResult>([]);
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
index 13ef5d6f64..ed90b3b1ae 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -76,16 +77,21 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected abstract APIRequest CreateScoreRequest();
- protected sealed override APIRequest FetchScores(Action> scoresCallback)
+ protected override async Task> FetchScores()
{
// This performs two requests:
// 1. A request to show the relevant score (and scores around).
// 2. If that fails, a request to index the room starting from the highest score.
+ var requestTaskSource = new TaskCompletionSource();
var userScoreReq = CreateScoreRequest();
+ userScoreReq.Success += requestTaskSource.SetResult;
+ userScoreReq.Failure += requestTaskSource.SetException;
+ API.Queue(userScoreReq);
- userScoreReq.Success += userScore =>
+ try
{
+ var userScore = await requestTaskSource.Task;
var allScores = new List { userScore };
// Other scores could have arrived between score submission and entering the results screen. Ensure the local player score position is up to date.
@@ -113,88 +119,96 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
setPositions(lowerScores, userScore.Position.Value, 1);
}
- Schedule(() =>
- {
- PerformSuccessCallback(scoresCallback, allScores);
- hideLoadingSpinners();
- });
- };
-
- // On failure, fallback to a normal index.
- userScoreReq.Failure += _ => API.Queue(createIndexRequest(scoresCallback));
-
- return userScoreReq;
+ return TransformScores(allScores);
+ }
+ catch (OperationCanceledException)
+ {
+ return [];
+ }
+ catch
+ {
+ return await fetchScoresAround();
+ }
+ finally
+ {
+ Schedule(() => hideLoadingSpinners());
+ }
}
- protected override APIRequest? FetchNextPage(int direction, Action> scoresCallback)
+ protected override async Task> FetchNextPage(int direction)
{
Debug.Assert(direction == 1 || direction == -1);
MultiplayerScores? pivot = direction == -1 ? higherScores : lowerScores;
-
if (pivot?.Cursor == null)
- return null;
+ return [];
- if (pivot == higherScores)
- LeftSpinner.Show();
- else
- RightSpinner.Show();
+ Schedule(() =>
+ {
+ if (pivot == higherScores)
+ LeftSpinner.Show();
+ else
+ RightSpinner.Show();
+ });
- return createIndexRequest(scoresCallback, pivot);
+ return await fetchScoresAround(pivot);
}
///
/// Creates a with an optional score pivot.
///
/// Does not queue the request.
- /// The callback to perform with the resulting scores.
/// An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score.
- /// The indexing .
- private APIRequest createIndexRequest(Action> scoresCallback, MultiplayerScores? pivot = null)
+ private async Task> fetchScoresAround(MultiplayerScores? pivot = null)
{
+ var requestTaskSource = new TaskCompletionSource();
var indexReq = pivot != null
? new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID, pivot.Cursor, pivot.Params)
: new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID);
+ indexReq.Success += requestTaskSource.SetResult;
+ indexReq.Failure += requestTaskSource.SetException;
+ API.Queue(indexReq);
- indexReq.Success += r =>
+ try
{
+ var index = await requestTaskSource.Task;
+
if (pivot == lowerScores)
{
- lowerScores = r;
- setPositions(r, pivot, 1);
+ lowerScores = index;
+ setPositions(index, pivot, 1);
}
else
{
- higherScores = r;
- setPositions(r, pivot, -1);
+ higherScores = index;
+ setPositions(index, pivot, -1);
}
- Schedule(() =>
- {
- PerformSuccessCallback(scoresCallback, r.Scores, r);
- hideLoadingSpinners(r);
- });
- };
-
- indexReq.Failure += _ => hideLoadingSpinners(pivot);
-
- return indexReq;
+ return TransformScores(index.Scores, index);
+ }
+ catch (OperationCanceledException)
+ {
+ return [];
+ }
+ finally
+ {
+ Schedule(() => hideLoadingSpinners(pivot));
+ }
}
///
/// Transforms returned into s, ensure the is put into a sane state, and invokes a given success callback.
///
- /// The callback to invoke with the final s.
/// The s that were retrieved from s.
/// An optional pivot around which the scores were retrieved.
- protected virtual ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null)
+ protected virtual ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null)
{
- var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
-
- // Invoke callback to add the scores. Exclude the score provided to this screen since it's added already.
- callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID));
-
- return scoreInfos;
+ // Exclude the score provided to this screen since it's added already.
+ return scores
+ .Where(s => s.ID != Score?.OnlineID)
+ .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo))
+ .OrderByTotalScore()
+ .ToArray();
}
private void hideLoadingSpinners(MultiplayerScores? pivot = null)
@@ -213,7 +227,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
/// The to set positions on.
/// The pivot.
/// The amount to increment the pivot position by for each in .
- private void setPositions(MultiplayerScores scores, MultiplayerScores? pivot, int increment)
+ private static void setPositions(MultiplayerScores scores, MultiplayerScores? pivot, int increment)
=> setPositions(scores, pivot?.Scores[^1].Position ?? 0, increment);
///
@@ -222,7 +236,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
/// The to set positions on.
/// The pivot position.
/// The amount to increment the pivot position by for each in .
- private void setPositions(MultiplayerScores scores, int pivotPosition, int increment)
+ private static void setPositions(MultiplayerScores scores, int pivotPosition, int increment)
{
foreach (var s in scores.Scores)
{
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs
index 05c03a4b28..c6c10e4d91 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.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.
-using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Online.API;
@@ -31,9 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId);
- protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null)
+ protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null)
{
- var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot);
+ var scoreInfos = base.TransformScores(scores, pivot);
Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId));
return scoreInfos;
}
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs
index 5b20496dba..1a0df0291c 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.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.
-using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Online.API;
@@ -25,9 +24,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId);
- protected override ScoreInfo[] PerformSuccessCallback(Action> callback, List scores, MultiplayerScores? pivot = null)
+ protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null)
{
- var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot);
+ var scoreInfos = base.TransformScores(scores, pivot);
Schedule(() =>
{
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index fe0d805cee..11e90a06b9 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -5,10 +5,12 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -23,7 +25,6 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
-using osu.Game.Online.API;
using osu.Game.Online.Placeholders;
using osu.Game.Overlays;
using osu.Game.Scoring;
@@ -60,9 +61,6 @@ namespace osu.Game.Screens.Ranking
private bool skipExitTransition;
- [Resolved]
- private IAPIProvider api { get; set; } = null!;
-
protected StatisticsPanel StatisticsPanel { get; private set; } = null!;
private Drawable bottomPanel = null!;
@@ -237,10 +235,7 @@ namespace osu.Game.Screens.Ranking
{
base.LoadComplete();
- var req = FetchScores(fetchScoresCallback);
-
- if (req != null)
- api.Queue(req);
+ FetchScores().ContinueWith(t => addScores(t.GetResultSafely()));
StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true);
}
@@ -251,18 +246,16 @@ namespace osu.Game.Screens.Ranking
if (lastFetchCompleted)
{
- APIRequest? nextPageRequest = null;
+ Task> nextPageTask = Task.FromResult>([]);
if (ScorePanelList.IsScrolledToStart)
- nextPageRequest = FetchNextPage(-1, fetchScoresCallback);
+ nextPageTask = FetchNextPage(-1);
else if (ScorePanelList.IsScrolledToEnd)
- nextPageRequest = FetchNextPage(1, fetchScoresCallback);
+ nextPageTask = FetchNextPage(1);
- if (nextPageRequest != null)
- {
- lastFetchCompleted = false;
- api.Queue(nextPageRequest);
- }
+ nextPageTask.ContinueWith(t => addScores(t.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion);
+
+ lastFetchCompleted = nextPageTask.IsCompletedSuccessfully;
}
}
@@ -329,17 +322,13 @@ namespace osu.Game.Screens.Ranking
///
/// Performs a fetch/refresh of scores to be displayed.
///
- /// A callback which should be called when fetching is completed. Scheduling is not required.
- /// An responsible for the fetch operation. This will be queued and performed automatically.
- protected virtual APIRequest? FetchScores(Action> scoresCallback) => null;
+ protected virtual Task> FetchScores() => Task.FromResult>([]);
///
- /// Performs a fetch of the next page of scores. This is invoked every frame until a non-null is returned.
+ /// Performs a fetch of the next page of scores. This is invoked every frame.
///
/// The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list.
- /// A callback which should be called when fetching is completed. Scheduling is not required.
- /// An responsible for the fetch operation. This will be queued and performed automatically.
- protected virtual APIRequest? FetchNextPage(int direction, Action> scoresCallback) => null;
+ protected virtual Task> FetchNextPage(int direction) => Task.FromResult>([]);
///
/// Creates the to be used to display extended information about scores.
@@ -351,10 +340,14 @@ namespace osu.Game.Screens.Ranking
: new StatisticsPanel();
}
- private void fetchScoresCallback(IEnumerable scores) => Schedule(() =>
+ private void addScores(IEnumerable scores) => Schedule(() =>
{
foreach (var s in scores)
- addScore(s);
+ {
+ var panel = ScorePanelList.AddScore(s);
+ if (detachedPanel != null)
+ panel.Alpha = 0;
+ }
// allow a frame for scroll container to adjust its dimensions with the added scores before fetching again.
Schedule(() => lastFetchCompleted = true);
@@ -409,14 +402,6 @@ namespace osu.Game.Screens.Ranking
return false;
}
- private void addScore(ScoreInfo score)
- {
- var panel = ScorePanelList.AddScore(score);
-
- if (detachedPanel != null)
- panel.Alpha = 0;
- }
-
private ScorePanel? detachedPanel;
private void onStatisticsStateChanged(ValueChangedEvent state)
diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
index 9f7604aa82..0593d5f91f 100644
--- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
@@ -4,11 +4,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Scoring;
@@ -21,26 +23,36 @@ namespace osu.Game.Screens.Ranking
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
+ [Resolved]
+ private IAPIProvider api { get; set; } = null!;
+
public SoloResultsScreen(ScoreInfo score)
: base(score)
{
}
- protected override APIRequest? FetchScores(Action> scoresCallback)
+ protected override async Task> FetchScores()
{
Debug.Assert(Score != null);
if (Score.BeatmapInfo!.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending)
- return null;
+ return [];
+
+ var requestTaskSource = new TaskCompletionSource();
getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset);
- getScoreRequest.Success += r =>
+ getScoreRequest.Success += requestTaskSource.SetResult;
+ getScoreRequest.Failure += requestTaskSource.SetException;
+ api.Queue(getScoreRequest);
+
+ try
{
+ var scores = await requestTaskSource.Task;
var toDisplay = new List();
- for (int i = 0; i < r.Scores.Count; ++i)
+ for (int i = 0; i < scores.Scores.Count; ++i)
{
- var score = r.Scores[i];
+ var score = scores.Scores[i];
int position = i + 1;
if (score.MatchesOnlineID(Score))
@@ -58,9 +70,12 @@ namespace osu.Game.Screens.Ranking
}
}
- scoresCallback.Invoke(toDisplay);
- };
- return getScoreRequest;
+ return toDisplay;
+ }
+ catch (OperationCanceledException)
+ {
+ return [];
+ }
}
protected override void Dispose(bool isDisposing)
From dfae11101f8b968611a442691b794066a52538c7 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Tue, 25 Feb 2025 22:37:12 +0900
Subject: [PATCH 108/262] Populate playlists results screen with online
beatmaps
---
osu.Game/Online/Rooms/MultiplayerScore.cs | 3 +++
.../Playlists/PlaylistItemResultsScreen.cs | 26 ++++++++++++++++---
.../PlaylistItemScoreResultsScreen.cs | 5 ++--
.../PlaylistItemUserBestResultsScreen.cs | 5 ++--
4 files changed, 31 insertions(+), 8 deletions(-)
diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs
index 2adee26da3..74eaea8dbc 100644
--- a/osu.Game/Online/Rooms/MultiplayerScore.cs
+++ b/osu.Game/Online/Rooms/MultiplayerScore.cs
@@ -80,6 +80,9 @@ namespace osu.Game.Online.Rooms
[JsonProperty("ruleset_id")]
public int RulesetId { get; set; }
+ [JsonProperty("beatmap_id")]
+ public int BeatmapId { get; set; }
+
public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, [NotNull] BeatmapInfo beatmap)
{
var ruleset = rulesets.GetRuleset(RulesetId);
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
index ed90b3b1ae..bba30ec312 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
@@ -9,8 +9,11 @@ using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Scoring;
@@ -39,6 +42,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved]
protected RulesetStore Rulesets { get; private set; } = null!;
+ [Resolved]
+ private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
+
protected PlaylistItemResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem)
: base(score)
{
@@ -119,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
setPositions(lowerScores, userScore.Position.Value, 1);
}
- return TransformScores(allScores);
+ return await TransformScores(allScores);
}
catch (OperationCanceledException)
{
@@ -184,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
setPositions(index, pivot, -1);
}
- return TransformScores(index.Scores, index);
+ return await TransformScores(index.Scores, index);
}
catch (OperationCanceledException)
{
@@ -201,12 +207,24 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
///
/// The s that were retrieved from s.
/// An optional pivot around which the scores were retrieved.
- protected virtual ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null)
+ protected virtual async Task TransformScores(List scores, MultiplayerScores? pivot = null)
{
+ APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray());
+
+ // Minimal data required to get various components in this screen to display correctly.
+ Dictionary beatmapsById = beatmaps.Where(b => b != null).ToDictionary(b => b!.OnlineID, b => new BeatmapInfo
+ {
+ Difficulty = new BeatmapDifficulty(b!.Difficulty),
+ DifficultyName = b.DifficultyName,
+ StarRating = b.StarRating,
+ Length = b.Length,
+ BPM = b.BPM
+ });
+
// Exclude the score provided to this screen since it's added already.
return scores
.Where(s => s.ID != Score?.OnlineID)
- .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo))
+ .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, beatmapsById.GetValueOrDefault(s.BeatmapId) ?? Beatmap.Value.BeatmapInfo))
.OrderByTotalScore()
.ToArray();
}
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs
index c6c10e4d91..f74b30c3f7 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Scoring;
@@ -30,9 +31,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId);
- protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null)
+ protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null)
{
- var scoreInfos = base.TransformScores(scores, pivot);
+ var scoreInfos = await base.TransformScores(scores, pivot);
Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId));
return scoreInfos;
}
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs
index 1a0df0291c..2e763666a7 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Scoring;
@@ -24,9 +25,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId);
- protected override ScoreInfo[] TransformScores(List scores, MultiplayerScores? pivot = null)
+ protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null)
{
- var scoreInfos = base.TransformScores(scores, pivot);
+ var scoreInfos = await base.TransformScores(scores, pivot);
Schedule(() =>
{
From 8a27b6689edf50cace897a3009640ff1ba8b2e7e Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Tue, 25 Feb 2025 22:51:36 +0900
Subject: [PATCH 109/262] Replace virtual async method with better abstraction
---
.../Playlists/PlaylistItemResultsScreen.cs | 9 ++++-----
.../Playlists/PlaylistItemScoreResultsScreen.cs | 8 +++-----
.../Playlists/PlaylistItemUserBestResultsScreen.cs | 14 ++++----------
osu.Game/Screens/Ranking/ResultsScreen.cs | 10 ++++++++++
4 files changed, 21 insertions(+), 20 deletions(-)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
index bba30ec312..e9ba3bdb70 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
@@ -125,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
setPositions(lowerScores, userScore.Position.Value, 1);
}
- return await TransformScores(allScores);
+ return await transformScores(allScores);
}
catch (OperationCanceledException)
{
@@ -190,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
setPositions(index, pivot, -1);
}
- return await TransformScores(index.Scores, index);
+ return await transformScores(index.Scores);
}
catch (OperationCanceledException)
{
@@ -203,11 +203,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}
///
- /// Transforms returned into s, ensure the is put into a sane state, and invokes a given success callback.
+ /// Transforms returned into s.
///
/// The s that were retrieved from s.
- /// An optional pivot around which the scores were retrieved.
- protected virtual async Task TransformScores(List scores, MultiplayerScores? pivot = null)
+ private async Task transformScores(List scores)
{
APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray());
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs
index f74b30c3f7..7f386cd293 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
-using System.Threading.Tasks;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Scoring;
@@ -31,11 +30,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId);
- protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null)
+ protected override void OnScoresAdded(IEnumerable scores)
{
- var scoreInfos = await base.TransformScores(scores, pivot);
- Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(s => s.OnlineID == scoreId));
- return scoreInfos;
+ base.OnScoresAdded(scores);
+ SelectedScore.Value ??= scores.SingleOrDefault(s => s.OnlineID == scoreId);
}
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs
index 2e763666a7..faeef93b71 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
-using System.Threading.Tasks;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Scoring;
@@ -25,17 +24,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId);
- protected override async Task TransformScores(List scores, MultiplayerScores? pivot = null)
+ protected override void OnScoresAdded(IEnumerable scores)
{
- var scoreInfos = await base.TransformScores(scores, pivot);
+ base.OnScoresAdded(scores);
- Schedule(() =>
- {
- // Prefer selecting the local user's score, or otherwise default to the first visible score.
- SelectedScore.Value ??= scoreInfos.FirstOrDefault(s => s.UserID == userId) ?? scoreInfos.FirstOrDefault();
- });
-
- return scoreInfos;
+ // Prefer selecting the local user's score, or otherwise default to the first visible score.
+ SelectedScore.Value ??= scores.FirstOrDefault(s => s.UserID == userId) ?? scores.FirstOrDefault();
}
}
}
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 11e90a06b9..ce86ac0815 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -357,8 +357,18 @@ namespace osu.Game.Screens.Ranking
// This can happen if for example a beatmap that is part of a playlist hasn't been played yet.
VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet));
}
+
+ OnScoresAdded(scores);
});
+ ///
+ /// Invoked after online scores are fetched and added to the list.
+ ///
+ /// The scores that were added.
+ protected virtual void OnScoresAdded(IEnumerable scores)
+ {
+ }
+
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(e);
From 3b5bf391da57e4ed3efcfd60f6e6fd3724f35b6d Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Tue, 25 Feb 2025 22:55:55 +0900
Subject: [PATCH 110/262] Arrays instead of enumerables
---
.../Visual/Ranking/TestSceneResultsScreen.cs | 21 +++++++++----------
.../Spectate/MultiSpectatorResultsScreen.cs | 5 ++---
.../Playlists/PlaylistItemResultsScreen.cs | 6 +++---
.../PlaylistItemScoreResultsScreen.cs | 3 +--
.../PlaylistItemUserBestResultsScreen.cs | 3 +--
osu.Game/Screens/Ranking/ResultsScreen.cs | 10 ++++-----
osu.Game/Screens/Ranking/SoloResultsScreen.cs | 4 ++--
7 files changed, 24 insertions(+), 28 deletions(-)
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index 4acbdb4a76..b19288fd99 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -4,7 +4,6 @@
#nullable disable
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
@@ -415,19 +414,19 @@ namespace osu.Game.Tests.Visual.Ranking
RetryOverlay = InternalChildren.OfType().SingleOrDefault();
}
- protected override Task> FetchScores()
+ protected override Task FetchScores()
{
- var scores = new List();
+ var scores = new ScoreInfo[20];
- for (int i = 0; i < 20; i++)
+ for (int i = 0; i < scores.Length; i++)
{
var score = TestResources.CreateTestScoreInfo();
score.TotalScore += 10 - i;
score.HasOnlineReplay = true;
- scores.Add(score);
+ scores[i] = score;
}
- return Task.FromResult>(scores);
+ return Task.FromResult(scores);
}
}
@@ -443,19 +442,19 @@ namespace osu.Game.Tests.Visual.Ranking
this.fetchWaitTask = fetchWaitTask ?? Task.CompletedTask;
}
- protected override Task> FetchScores()
+ protected override Task FetchScores()
{
- return Task.Run>(async () =>
+ return Task.Run(async () =>
{
await fetchWaitTask;
- var scores = new List();
+ var scores = new ScoreInfo[20];
- for (int i = 0; i < 20; i++)
+ for (int i = 0; i < scores.Length; i++)
{
var score = TestResources.CreateTestScoreInfo();
score.TotalScore += 10 - i;
- scores.Add(score);
+ scores[i] = score;
}
Schedule(() => FetchCompleted = true);
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs
index 6e2f90e3b5..3cf1661c8d 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorResultsScreen.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.
-using System.Collections.Generic;
using System.Threading.Tasks;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
@@ -22,8 +21,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
Scheduler.AddDelayed(() => StatisticsPanel.ToggleVisibility(), 1000);
}
- protected override Task> FetchScores() => Task.FromResult>([]);
+ protected override Task FetchScores() => Task.FromResult([]);
- protected override Task> FetchNextPage(int direction) => Task.FromResult>([]);
+ protected override Task FetchNextPage(int direction) => Task.FromResult([]);
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
index e9ba3bdb70..0063bcd5f5 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
@@ -83,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected abstract APIRequest CreateScoreRequest();
- protected override async Task> FetchScores()
+ protected override async Task FetchScores()
{
// This performs two requests:
// 1. A request to show the relevant score (and scores around).
@@ -141,7 +141,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}
}
- protected override async Task> FetchNextPage(int direction)
+ protected override async Task FetchNextPage(int direction)
{
Debug.Assert(direction == 1 || direction == -1);
@@ -165,7 +165,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
///
/// Does not queue the request.
/// An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score.
- private async Task> fetchScoresAround(MultiplayerScores? pivot = null)
+ private async Task fetchScoresAround(MultiplayerScores? pivot = null)
{
var requestTaskSource = new TaskCompletionSource();
var indexReq = pivot != null
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs
index 7f386cd293..74b12b6d3c 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemScoreResultsScreen.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.
-using System.Collections.Generic;
using System.Linq;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@@ -30,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override APIRequest CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId);
- protected override void OnScoresAdded(IEnumerable scores)
+ protected override void OnScoresAdded(ScoreInfo[] scores)
{
base.OnScoresAdded(scores);
SelectedScore.Value ??= scores.SingleOrDefault(s => s.OnlineID == scoreId);
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs
index faeef93b71..866b094178 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemUserBestResultsScreen.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.
-using System.Collections.Generic;
using System.Linq;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@@ -24,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override APIRequest CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, userId);
- protected override void OnScoresAdded(IEnumerable scores)
+ protected override void OnScoresAdded(ScoreInfo[] scores)
{
base.OnScoresAdded(scores);
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index ce86ac0815..cfee2aa77d 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -246,7 +246,7 @@ namespace osu.Game.Screens.Ranking
if (lastFetchCompleted)
{
- Task> nextPageTask = Task.FromResult>([]);
+ Task nextPageTask = Task.FromResult([]);
if (ScorePanelList.IsScrolledToStart)
nextPageTask = FetchNextPage(-1);
@@ -322,13 +322,13 @@ namespace osu.Game.Screens.Ranking
///
/// Performs a fetch/refresh of scores to be displayed.
///
- protected virtual Task> FetchScores() => Task.FromResult>([]);
+ protected virtual Task FetchScores() => Task.FromResult([]);
///
/// Performs a fetch of the next page of scores. This is invoked every frame.
///
/// The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list.
- protected virtual Task> FetchNextPage(int direction) => Task.FromResult>([]);
+ protected virtual Task FetchNextPage(int direction) => Task.FromResult([]);
///
/// Creates the to be used to display extended information about scores.
@@ -340,7 +340,7 @@ namespace osu.Game.Screens.Ranking
: new StatisticsPanel();
}
- private void addScores(IEnumerable scores) => Schedule(() =>
+ private void addScores(ScoreInfo[] scores) => Schedule(() =>
{
foreach (var s in scores)
{
@@ -365,7 +365,7 @@ namespace osu.Game.Screens.Ranking
/// Invoked after online scores are fetched and added to the list.
///
/// The scores that were added.
- protected virtual void OnScoresAdded(IEnumerable scores)
+ protected virtual void OnScoresAdded(ScoreInfo[] scores)
{
}
diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
index 0593d5f91f..9fdffce644 100644
--- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Screens.Ranking
{
}
- protected override async Task> FetchScores()
+ protected override async Task FetchScores()
{
Debug.Assert(Score != null);
@@ -70,7 +70,7 @@ namespace osu.Game.Screens.Ranking
}
}
- return toDisplay;
+ return toDisplay.ToArray();
}
catch (OperationCanceledException)
{
From 116b5a335a658023e3b58d3ec5caedd78230a3d4 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Tue, 25 Feb 2025 22:56:38 +0900
Subject: [PATCH 111/262] `ConfigureAwait(false)` everywhere
---
.../Playlists/PlaylistItemResultsScreen.cs | 14 +++++++-------
osu.Game/Screens/Ranking/SoloResultsScreen.cs | 2 +-
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
index 0063bcd5f5..975cff0b68 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
@@ -97,7 +97,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
try
{
- var userScore = await requestTaskSource.Task;
+ var userScore = await requestTaskSource.Task.ConfigureAwait(false);
var allScores = new List { userScore };
// Other scores could have arrived between score submission and entering the results screen. Ensure the local player score position is up to date.
@@ -125,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
setPositions(lowerScores, userScore.Position.Value, 1);
}
- return await transformScores(allScores);
+ return await transformScores(allScores).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -133,7 +133,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}
catch
{
- return await fetchScoresAround();
+ return await fetchScoresAround().ConfigureAwait(false);
}
finally
{
@@ -157,7 +157,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
RightSpinner.Show();
});
- return await fetchScoresAround(pivot);
+ return await fetchScoresAround(pivot).ConfigureAwait(false);
}
///
@@ -177,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
try
{
- var index = await requestTaskSource.Task;
+ var index = await requestTaskSource.Task.ConfigureAwait(false);
if (pivot == lowerScores)
{
@@ -190,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
setPositions(index, pivot, -1);
}
- return await transformScores(index.Scores);
+ return await transformScores(index.Scores).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -208,7 +208,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
/// The s that were retrieved from s.
private async Task transformScores(List scores)
{
- APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray());
+ APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()).ConfigureAwait(false);
// Minimal data required to get various components in this screen to display correctly.
Dictionary beatmapsById = beatmaps.Where(b => b != null).ToDictionary(b => b!.OnlineID, b => new BeatmapInfo
diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
index 9fdffce644..73bed3383b 100644
--- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking
try
{
- var scores = await requestTaskSource.Task;
+ var scores = await requestTaskSource.Task.ConfigureAwait(false);
var toDisplay = new List();
for (int i = 0; i < scores.Scores.Count; ++i)
From bb457ca8e2fa2283d159fd214f6854046f38cebb Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Tue, 25 Feb 2025 23:17:02 +0900
Subject: [PATCH 112/262] Clean up completion handling
---
osu.Game/Screens/Ranking/ResultsScreen.cs | 51 +++++++++++++----------
1 file changed, 28 insertions(+), 23 deletions(-)
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index cfee2aa77d..397ad9c0b1 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -10,7 +10,6 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
-using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -66,7 +65,7 @@ namespace osu.Game.Screens.Ranking
private Drawable bottomPanel = null!;
private Container detachedPanelContainer = null!;
- private bool lastFetchCompleted;
+ private Task lastFetchTask = Task.CompletedTask;
///
/// Whether the user can retry the beatmap from the results screen.
@@ -235,7 +234,7 @@ namespace osu.Game.Screens.Ranking
{
base.LoadComplete();
- FetchScores().ContinueWith(t => addScores(t.GetResultSafely()));
+ lastFetchTask = Task.Run(async () => await addScores(await FetchScores().ConfigureAwait(false)).ConfigureAwait(false));
StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true);
}
@@ -244,18 +243,17 @@ namespace osu.Game.Screens.Ranking
{
base.Update();
- if (lastFetchCompleted)
+ if (lastFetchTask.IsCompleted)
{
- Task nextPageTask = Task.FromResult([]);
+ Task? nextPageTask = null;
if (ScorePanelList.IsScrolledToStart)
nextPageTask = FetchNextPage(-1);
else if (ScorePanelList.IsScrolledToEnd)
nextPageTask = FetchNextPage(1);
- nextPageTask.ContinueWith(t => addScores(t.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion);
-
- lastFetchCompleted = nextPageTask.IsCompletedSuccessfully;
+ if (nextPageTask != null)
+ lastFetchTask = Task.Run(async () => await addScores(await nextPageTask).ConfigureAwait(false));
}
}
@@ -340,26 +338,33 @@ namespace osu.Game.Screens.Ranking
: new StatisticsPanel();
}
- private void addScores(ScoreInfo[] scores) => Schedule(() =>
+ private Task addScores(ScoreInfo[] scores)
{
- foreach (var s in scores)
+ var tcs = new TaskCompletionSource();
+
+ Schedule(() =>
{
- var panel = ScorePanelList.AddScore(s);
- if (detachedPanel != null)
- panel.Alpha = 0;
- }
+ foreach (var s in scores)
+ {
+ var panel = ScorePanelList.AddScore(s);
+ if (detachedPanel != null)
+ panel.Alpha = 0;
+ }
- // allow a frame for scroll container to adjust its dimensions with the added scores before fetching again.
- Schedule(() => lastFetchCompleted = true);
+ // allow a frame for scroll container to adjust its dimensions with the added scores before fetching again.
+ Schedule(() => tcs.SetResult());
- if (ScorePanelList.IsEmpty)
- {
- // This can happen if for example a beatmap that is part of a playlist hasn't been played yet.
- VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet));
- }
+ if (ScorePanelList.IsEmpty)
+ {
+ // This can happen if for example a beatmap that is part of a playlist hasn't been played yet.
+ VerticalScrollContent.Add(new MessagePlaceholder(LeaderboardStrings.NoRecordsYet));
+ }
- OnScoresAdded(scores);
- });
+ OnScoresAdded(scores);
+ });
+
+ return tcs.Task;
+ }
///
/// Invoked after online scores are fetched and added to the list.
From baf20d84843071e7c1c26418da36d1f4ff5c5a21 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Tue, 25 Feb 2025 23:17:23 +0900
Subject: [PATCH 113/262] Fix loading spinners not hiding correctly
---
.../Playlists/PlaylistItemResultsScreen.cs | 19 +++++--------------
1 file changed, 5 insertions(+), 14 deletions(-)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
index 975cff0b68..f08b1818ab 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
@@ -135,10 +135,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
return await fetchScoresAround().ConfigureAwait(false);
}
- finally
- {
- Schedule(() => hideLoadingSpinners());
- }
}
protected override async Task FetchNextPage(int direction)
@@ -196,10 +192,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
return [];
}
- finally
- {
- Schedule(() => hideLoadingSpinners(pivot));
- }
}
///
@@ -228,14 +220,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
.ToArray();
}
- private void hideLoadingSpinners(MultiplayerScores? pivot = null)
+ protected override void OnScoresAdded(ScoreInfo[] scores)
{
- CentreSpinner.Hide();
+ base.OnScoresAdded(scores);
- if (pivot == lowerScores)
- RightSpinner.Hide();
- else if (pivot == higherScores)
- LeftSpinner.Hide();
+ CentreSpinner.Hide();
+ RightSpinner.Hide();
+ LeftSpinner.Hide();
}
///
From 65a62d5440b57440b61578981691fd7bb6f2fb70 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Tue, 25 Feb 2025 15:38:48 +0100
Subject: [PATCH 114/262] Attempt to preserve sample control point bank when
encoding beatmap
This was reported internally in
https://discord.com/channels/90072389919997952/1259818301517725707/1343470899357024286.
The issue described was that sample specifications on control points in
stable disappeared after the beatmap was updated from lazer.
The reason why the sample specifications were getting dropped is that
they got lost in the logic that attempts to translate per-hitobject
samples that lazer has back into stable "green line" type control
points. That process only attempted to preserve volume and custom sample
bank, but did not keep the standard bank - likely because it's kind of
superfluous information *for correct sample playback of the objects*, as
the samples get encoded again for each object individually. However
dropping this information makes for a subpar editing experience.
The choice of which sample to pick the bank from is sort of arbitrary
and I'm not sure if there's a correct one to pick. Intuitively picking
the normal sample's bank (if there is one) seems most correct.
---
osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index 07e88ab956..d80d7e6b09 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -319,11 +319,13 @@ namespace osu.Game.Beatmaps.Formats
SampleControlPoint createSampleControlPointFor(double time, IList samples)
{
int volume = samples.Max(o => o.Volume);
+ string bank = samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).Select(s => s.Bank).FirstOrDefault()
+ ?? samples.Select(s => s.Bank).First();
int customIndex = samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo)
? samples.OfType().Max(o => o.CustomSampleBank)
: -1;
- return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = time, SampleVolume = volume, CustomSampleBank = customIndex };
+ return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = time, SampleVolume = volume, SampleBank = bank, CustomSampleBank = customIndex };
}
}
From 90290997a7b754a2506a4c10a8cc28cb3a0e33bd Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 26 Feb 2025 14:46:37 +0900
Subject: [PATCH 115/262] Fix score panel difficulty depending on local beatmap
This is a very special case where online beatmap/ruleset models are
being ferried via `ScoreInfo` in what appear to `BeatmapDifficultyCache`
as local `BeatmapInfo`/`RulesetInfo` models. Here, BDC will incorrectly
attempt to proceed with calculating true difficulty where it cannot, and
return 0.
This is fixed locally because `ScoreInfo` is a very weird model, and I'm
not sure whether BDC should contain logic to work around this.
---
.../Expanded/ExpandedPanelMiddleContent.cs | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index 4bc559694a..9bef6a3f3a 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.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;
@@ -16,6 +14,7 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Configuration;
+using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -41,10 +40,10 @@ namespace osu.Game.Screens.Ranking.Expanded
private readonly List statisticDisplays = new List();
- private RollingCounter scoreCounter;
+ private RollingCounter scoreCounter = null!;
[Resolved]
- private ScoreManager scoreManager { get; set; }
+ private ScoreManager scoreManager { get; set; } = null!;
///
/// Creates a new .
@@ -63,12 +62,19 @@ namespace osu.Game.Screens.Ranking.Expanded
}
[BackgroundDependencyLoader]
- private void load(BeatmapDifficultyCache beatmapDifficultyCache)
+ private void load(RealmAccess realmAccess, BeatmapDifficultyCache beatmapDifficultyCache)
{
var beatmap = score.BeatmapInfo!;
var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata;
string creator = metadata.Author.Username;
+ StarDifficulty starDifficulty = new StarDifficulty(beatmap.StarRating, 0);
+
+ // In some cases, the beatmap ferried through ScoreInfo actually represents an online beatmap.
+ // If it isn't, we may be able to compute a more accuracy difficulty from the ruleset and mods.
+ if (realmAccess.Run(r => r.Find(score.BeatmapInfo!.ID)) != null)
+ starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods).GetResultSafely() ?? starDifficulty;
+
var topStatistics = new List
{
new AccuracyStatistic(score.Accuracy),
@@ -146,7 +152,7 @@ namespace osu.Game.Screens.Ranking.Expanded
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
- new StarRatingDisplay(beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely() ?? default)
+ new StarRatingDisplay(starDifficulty)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
From 59cfcb3595aa79ea4384bca9af4472b48ace3917 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 26 Feb 2025 14:49:38 +0900
Subject: [PATCH 116/262] Prefer local models where available
---
.../Playlists/PlaylistItemResultsScreen.cs | 52 +++++++++++++++----
1 file changed, 42 insertions(+), 10 deletions(-)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
index f08b1818ab..1be0a7cf81 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
@@ -45,6 +45,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
+ [Resolved]
+ private BeatmapManager beatmapManager { get; set; } = null!;
+
protected PlaylistItemResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem)
: base(score)
{
@@ -200,22 +203,43 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
/// The s that were retrieved from s.
private async Task transformScores(List scores)
{
- APIBeatmap?[] beatmaps = await beatmapLookupCache.GetBeatmapsAsync(scores.Select(s => s.BeatmapId).Distinct().ToArray()).ConfigureAwait(false);
+ int[] allBeatmapIds = scores.Select(s => s.BeatmapId).Distinct().ToArray();
+ BeatmapInfo[] localBeatmaps = allBeatmapIds.Select(id => beatmapManager.QueryBeatmap(b => b.OnlineID == id))
+ .Where(b => b != null)
+ .ToArray()!;
- // Minimal data required to get various components in this screen to display correctly.
- Dictionary beatmapsById = beatmaps.Where(b => b != null).ToDictionary(b => b!.OnlineID, b => new BeatmapInfo
+ int[] missingBeatmapIds = allBeatmapIds.Except(localBeatmaps.Select(b => b.OnlineID)).ToArray();
+ APIBeatmap[] onlineBeatmaps = (await beatmapLookupCache.GetBeatmapsAsync(missingBeatmapIds).ConfigureAwait(false)).Where(b => b != null).ToArray()!;
+
+ Dictionary beatmapsById = new Dictionary();
+
+ foreach (var beatmap in localBeatmaps)
+ beatmapsById[beatmap.OnlineID] = beatmap;
+
+ foreach (var beatmap in onlineBeatmaps)
{
- Difficulty = new BeatmapDifficulty(b!.Difficulty),
- DifficultyName = b.DifficultyName,
- StarRating = b.StarRating,
- Length = b.Length,
- BPM = b.BPM
- });
+ // Minimal data required to get various components in this screen to display correctly.
+ beatmapsById[beatmap.OnlineID] = new BeatmapInfo
+ {
+ Difficulty = new BeatmapDifficulty(beatmap.Difficulty),
+ DifficultyName = beatmap.DifficultyName,
+ StarRating = beatmap.StarRating,
+ Length = beatmap.Length,
+ BPM = beatmap.BPM
+ };
+ }
+
+ // Validate that we have all beatmaps we need.
+ foreach (int id in allBeatmapIds)
+ {
+ if (!beatmapsById.ContainsKey(id))
+ throw new MissingBeatmapException(PlaylistItem, id);
+ }
// Exclude the score provided to this screen since it's added already.
return scores
.Where(s => s.ID != Score?.OnlineID)
- .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, beatmapsById.GetValueOrDefault(s.BeatmapId) ?? Beatmap.Value.BeatmapInfo))
+ .Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, beatmapsById[s.BeatmapId]))
.OrderByTotalScore()
.ToArray();
}
@@ -280,5 +304,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
X = (float)(list.ScrollableExtent - list.Current - panelOffset);
}
}
+
+ private class MissingBeatmapException : Exception
+ {
+ public MissingBeatmapException(PlaylistItem item, int beatmapId)
+ : base($"Missing beatmap {beatmapId} for playlist item {item.ID}")
+ {
+ }
+ }
}
}
From b7d431fdde61b56f6f1831c366163da54c71d021 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 26 Feb 2025 15:04:43 +0900
Subject: [PATCH 117/262] Include author
---
.../OnlinePlay/Playlists/PlaylistItemResultsScreen.cs | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
index 1be0a7cf81..53cd81b2a1 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
@@ -222,6 +223,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
beatmapsById[beatmap.OnlineID] = new BeatmapInfo
{
Difficulty = new BeatmapDifficulty(beatmap.Difficulty),
+ Metadata =
+ {
+ Author = new RealmUser
+ {
+ Username = beatmap.Metadata.Author.Username,
+ OnlineID = beatmap.Metadata.Author.OnlineID,
+ }
+ },
DifficultyName = beatmap.DifficultyName,
StarRating = beatmap.StarRating,
Length = beatmap.Length,
From abc12abdedfbb315996d5c16e5556cc9837d1e17 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Wed, 26 Feb 2025 16:48:18 +0900
Subject: [PATCH 118/262] Fix `PlayerTeamFlag` skinnable component not showing
team details during replay
For now, let's fetch on demand.
Note that song select local leaderboard has the same issue. I feel we should be
doing a lot more cached lookups (probaly with persisting across game restarts).
Maybe even replacing the realm user storage. An issue for another day.
---
osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs
index f8ef03c58c..3f72099a45 100644
--- a/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs
+++ b/osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs
@@ -3,8 +3,10 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Skinning;
@@ -40,10 +42,19 @@ namespace osu.Game.Screens.Play.HUD
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(UserLookupCache userLookupCache)
{
if (gameplayState != null)
- flag.Team = gameplayState.Score.ScoreInfo.User.Team;
+ {
+ if (gameplayState.Score.ScoreInfo.User.Team != null)
+ flag.Team = gameplayState.Score.ScoreInfo.User.Team;
+ else
+ {
+ // We only store very basic information about a user to realm, so there's a high chance we don't have the team information.
+ userLookupCache.GetUserAsync(gameplayState.Score.ScoreInfo.User.Id)
+ .ContinueWith(task => Schedule(() => flag.Team = task.GetResultSafely()?.Team));
+ }
+ }
else
{
apiUser = api.LocalUser.GetBoundCopy();
From e8b7ec0f9537db864b712ebc28ba63afabe3eeb3 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Wed, 26 Feb 2025 17:01:48 +0900
Subject: [PATCH 119/262] Adjust leaderboard score design slightly
This design is about to get replaced, so I'm just making some minor
adjustments since a lot of people complained about the font size in the
last update.
Of note, I'm only changing the font size which is one pt size lower than
we'd usually use. Also overlapping the mod icons to create a bit more
space (since there's already cases where they don't fit).
Closes https://github.com/ppy/osu/issues/32055 as far as I'm concerned.
I can read everything fine at 0.8x UI scale.
---
osu.Game/Online/Leaderboards/LeaderboardScore.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
index fc30f158f0..7306c2d21e 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -271,6 +271,7 @@ namespace osu.Game.Online.Leaderboards
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
+ Spacing = new Vector2(-10, 0),
Direction = FillDirection.Horizontal,
ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.34f) })
},
@@ -425,7 +426,7 @@ namespace osu.Game.Online.Leaderboards
public DateLabel(DateTimeOffset date)
: base(date)
{
- Font = OsuFont.GetFont(size: 13, weight: FontWeight.Bold, italics: true);
+ Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold);
}
protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30));
From c7fd7cf9cd4071123ea83fb479cb8e543cdb1a0c Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 26 Feb 2025 17:39:56 +0900
Subject: [PATCH 120/262] Add missing ConfigureAwait
---
osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 397ad9c0b1..26b13d026c 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -253,7 +253,7 @@ namespace osu.Game.Screens.Ranking
nextPageTask = FetchNextPage(1);
if (nextPageTask != null)
- lastFetchTask = Task.Run(async () => await addScores(await nextPageTask).ConfigureAwait(false));
+ lastFetchTask = Task.Run(async () => await addScores(await nextPageTask.ConfigureAwait(false)).ConfigureAwait(false));
}
}
From c45a403fe2b87db7b43d3500fe25e348b88e27ed Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Wed, 26 Feb 2025 18:00:18 +0900
Subject: [PATCH 121/262] Mostly revert sizes
---
osu.Game/Online/Leaderboards/LeaderboardScore.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
index 7306c2d21e..0db03efb68 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -395,7 +395,7 @@ namespace osu.Game.Online.Leaderboards
Origin = Anchor.CentreLeft,
Text = statistic.Value,
Spacing = new Vector2(-1, 0),
- Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, fixedWidth: true)
+ Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold, fixedWidth: true)
},
},
};
@@ -426,7 +426,7 @@ namespace osu.Game.Online.Leaderboards
public DateLabel(DateTimeOffset date)
: base(date)
{
- Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold);
+ Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold);
}
protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30));
From 3dde024650cc1564369dc0f23b462f876871400a Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 26 Feb 2025 18:00:16 +0900
Subject: [PATCH 122/262] Replace error handling with logs
- Handling all errors matches master a little bit better. Logging
exceptions in any case.
- Not throwing when beatmaps are missing simplifies tests.
---
.../Playlists/PlaylistItemResultsScreen.cs | 21 +++++++------------
osu.Game/Screens/Ranking/SoloResultsScreen.cs | 4 +++-
2 files changed, 10 insertions(+), 15 deletions(-)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
index 53cd81b2a1..572bf535f7 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
@@ -131,10 +132,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return await transformScores(allScores).ConfigureAwait(false);
}
- catch (OperationCanceledException)
- {
- return [];
- }
catch
{
return await fetchScoresAround().ConfigureAwait(false);
@@ -192,8 +189,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return await transformScores(index.Scores).ConfigureAwait(false);
}
- catch (OperationCanceledException)
+ catch (Exception ex)
{
+ Logger.Log($"Failed to fetch scores (room: {RoomId}, item: {PlaylistItem.ID}): {ex}");
return [];
}
}
@@ -242,7 +240,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
foreach (int id in allBeatmapIds)
{
if (!beatmapsById.ContainsKey(id))
- throw new MissingBeatmapException(PlaylistItem, id);
+ {
+ Logger.Log($"Failed to fetch beatmap {id} to display scores for playlist item {PlaylistItem.ID}");
+ beatmapsById[id] = Beatmap.Value.BeatmapInfo;
+ }
}
// Exclude the score provided to this screen since it's added already.
@@ -313,13 +314,5 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
X = (float)(list.ScrollableExtent - list.Current - panelOffset);
}
}
-
- private class MissingBeatmapException : Exception
- {
- public MissingBeatmapException(PlaylistItem item, int beatmapId)
- : base($"Missing beatmap {beatmapId} for playlist item {item.ID}")
- {
- }
- }
}
}
diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
index 73bed3383b..3486d81e8a 100644
--- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using osu.Framework.Allocation;
+using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Online.API;
@@ -72,8 +73,9 @@ namespace osu.Game.Screens.Ranking
return toDisplay.ToArray();
}
- catch (OperationCanceledException)
+ catch (Exception ex)
{
+ Logger.Log($"Failed to fetch scores (beatmap: {Score.BeatmapInfo}, ruleset: {Score.Ruleset}): {ex}");
return [];
}
}
From c280c8fa1c463c280aee473b6c987d46a271dd25 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 26 Feb 2025 18:31:06 +0900
Subject: [PATCH 123/262] Add support to tests
Somewhat informal because it isn't super easy to handle.
---
.../TestScenePlaylistsResultsScreen.cs | 36 ++++++++++++++++++-
1 file changed, 35 insertions(+), 1 deletion(-)
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index 33bd573617..dc5fb20e16 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -7,9 +7,12 @@ using System.Linq;
using System.Net;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
@@ -32,6 +35,9 @@ namespace osu.Game.Tests.Visual.Playlists
private const int scores_per_result = 10;
private const int real_user_position = 200;
+ [Cached]
+ private readonly BeatmapLookupCache beatmapLookupCache = new BeatmapLookupCache();
+
private ResultsScreen resultsScreen = null!;
private int lowestScoreId; // Score ID of the lowest score in the list.
@@ -41,6 +47,11 @@ namespace osu.Game.Tests.Visual.Playlists
private int totalCount;
private ScoreInfo userScore = null!;
+ public TestScenePlaylistsResultsScreen()
+ {
+ Add(beatmapLookupCache);
+ }
+
[SetUpSteps]
public override void SetUpSteps()
{
@@ -279,6 +290,25 @@ namespace osu.Game.Tests.Visual.Playlists
case IndexPlaylistScoresRequest:
break;
+ case GetBeatmapsRequest getBeatmaps:
+ getBeatmaps.TriggerSuccess(new GetBeatmapsResponse
+ {
+ Beatmaps = getBeatmaps.BeatmapIds.Select(id => new APIBeatmap
+ {
+ OnlineID = id,
+ StarRating = id,
+ DifficultyName = $"Beatmap {id}",
+ BeatmapSet = new APIBeatmapSet
+ {
+ Title = $"Title {id}",
+ Artist = $"Artist {id}",
+ AuthorString = $"Author {id}"
+ }
+ }).ToList()
+ });
+
+ return true;
+
default:
return false;
}
@@ -346,6 +376,7 @@ namespace osu.Game.Tests.Visual.Playlists
Position = real_user_position,
MaxCombo = userScore.MaxCombo,
User = userScore.User,
+ BeatmapId = RNG.Next(0, 7),
ScoresAround = new MultiplayerScoresAround
{
Higher = new MultiplayerScores(),
@@ -364,6 +395,7 @@ namespace osu.Game.Tests.Visual.Playlists
Passed = true,
Rank = userScore.Rank,
MaxCombo = userScore.MaxCombo,
+ BeatmapId = RNG.Next(0, 7),
User = new APIUser
{
Id = 2 + i,
@@ -379,6 +411,7 @@ namespace osu.Game.Tests.Visual.Playlists
Passed = true,
Rank = userScore.Rank,
MaxCombo = userScore.MaxCombo,
+ BeatmapId = RNG.Next(0, 7),
User = new APIUser
{
Id = 2 + i,
@@ -396,7 +429,7 @@ namespace osu.Game.Tests.Visual.Playlists
return multiplayerUserScore;
}
- private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores = false)
+ private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores)
{
var result = new IndexedMultiplayerScores();
@@ -413,6 +446,7 @@ namespace osu.Game.Tests.Visual.Playlists
Passed = true,
Rank = ScoreRank.X,
MaxCombo = 1000,
+ BeatmapId = RNG.Next(0, 7),
User = new APIUser
{
Id = 2 + i,
From 76bf03b05dd92938290c23631e00f82fc945f631 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Wed, 26 Feb 2025 10:56:28 +0100
Subject: [PATCH 124/262] Add failing decoder test case for too many combo
colours
---
.../Formats/LegacyBeatmapDecoderTest.cs | 29 ++++++++
.../Resources/too-many-combo-colours.osu | 73 +++++++++++++++++++
2 files changed, 102 insertions(+)
create mode 100644 osu.Game.Tests/Resources/too-many-combo-colours.osu
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index adb1755c11..9747b654ae 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -404,6 +404,35 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestComboColourCountIsLimitedToEight()
+ {
+ var decoder = new LegacySkinDecoder();
+
+ using (var resStream = TestResources.OpenResource("too-many-combo-colours.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var comboColors = decoder.Decode(stream).ComboColours;
+
+ Debug.Assert(comboColors != null);
+
+ Color4[] expectedColors =
+ {
+ new Color4(142, 199, 255, 255),
+ new Color4(255, 128, 128, 255),
+ new Color4(128, 255, 255, 255),
+ new Color4(128, 255, 128, 255),
+ new Color4(255, 187, 255, 255),
+ new Color4(255, 177, 140, 255),
+ new Color4(100, 100, 100, 255),
+ new Color4(142, 199, 255, 255),
+ };
+ Assert.AreEqual(expectedColors.Length, comboColors.Count);
+ for (int i = 0; i < expectedColors.Length; i++)
+ Assert.AreEqual(expectedColors[i], comboColors[i]);
+ }
+ }
+
[Test]
public void TestGetLastObjectTime()
{
diff --git a/osu.Game.Tests/Resources/too-many-combo-colours.osu b/osu.Game.Tests/Resources/too-many-combo-colours.osu
new file mode 100644
index 0000000000..477e362a6d
--- /dev/null
+++ b/osu.Game.Tests/Resources/too-many-combo-colours.osu
@@ -0,0 +1,73 @@
+osu file format v14
+
+[General]
+AudioFilename: 03. Renatus - Soleily 192kbps.mp3
+AudioLeadIn: 0
+PreviewTime: 164471
+Countdown: 0
+SampleSet: Soft
+StackLeniency: 0.7
+Mode: 0
+LetterboxInBreaks: 0
+WidescreenStoryboard: 0
+
+[Editor]
+Bookmarks: 11505,22054,32604,43153,53703,64252,74802,85351,95901,106450,116999,119637,130186,140735,151285,161834,164471,175020,185570,196119,206669,209306
+DistanceSpacing: 1.8
+BeatDivisor: 4
+GridSize: 4
+TimelineZoom: 2
+
+[Metadata]
+Title:Renatus
+TitleUnicode:Renatus
+Artist:Soleily
+ArtistUnicode:Soleily
+Creator:Gamu
+Version:Insane
+Source:
+Tags:MBC7 Unisphere 地球ヤバイEP Chikyu Yabai
+BeatmapID:557821
+BeatmapSetID:241526
+
+[Difficulty]
+HPDrainRate:6.5
+CircleSize:4
+OverallDifficulty:8
+ApproachRate:9
+SliderMultiplier:1.8
+SliderTickRate:2
+
+[Events]
+//Background and Video events
+0,0,"machinetop_background.jpg",0,0
+//Break Periods
+2,122474,140135
+//Storyboard Layer 0 (Background)
+//Storyboard Layer 1 (Fail)
+//Storyboard Layer 2 (Pass)
+//Storyboard Layer 3 (Foreground)
+//Storyboard Sound Samples
+
+[TimingPoints]
+956,329.67032967033,4,2,0,60,1,0
+
+
+[Colours]
+Combo1:142,199,255
+Combo2:255,128,128
+Combo3:128,255,255
+Combo4:128,255,128
+Combo5:255,187,255
+Combo6:255,177,140
+Combo7:100,100,100
+Combo8:142,199,255
+Combo9:255,128,128
+Combo10:128,255,255
+Combo11:128,255,128
+Combo12:255,187,255
+Combo13:255,177,140
+Combo14:100,100,100
+
+[HitObjects]
+192,168,956,6,0,P|184:128|200:80,1,90,4|0,1:2|0:0,0:0:0:0:
From c2875423eeb264752954ab56f01a8ec2f702510d Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Wed, 26 Feb 2025 18:58:29 +0900
Subject: [PATCH 125/262] Cleanup score fetching a bit
---
osu.Game/Screens/Ranking/ResultsScreen.cs | 51 ++++++++++++++++-------
1 file changed, 37 insertions(+), 14 deletions(-)
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 26b13d026c..010f7e1a93 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -234,27 +234,19 @@ namespace osu.Game.Screens.Ranking
{
base.LoadComplete();
- lastFetchTask = Task.Run(async () => await addScores(await FetchScores().ConfigureAwait(false)).ConfigureAwait(false));
-
StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true);
+
+ fetchScores(null);
}
protected override void Update()
{
base.Update();
- if (lastFetchTask.IsCompleted)
- {
- Task? nextPageTask = null;
-
- if (ScorePanelList.IsScrolledToStart)
- nextPageTask = FetchNextPage(-1);
- else if (ScorePanelList.IsScrolledToEnd)
- nextPageTask = FetchNextPage(1);
-
- if (nextPageTask != null)
- lastFetchTask = Task.Run(async () => await addScores(await nextPageTask.ConfigureAwait(false)).ConfigureAwait(false));
- }
+ if (ScorePanelList.IsScrolledToStart)
+ fetchScores(-1);
+ else if (ScorePanelList.IsScrolledToEnd)
+ fetchScores(1);
}
#region Applause
@@ -317,6 +309,37 @@ namespace osu.Game.Screens.Ranking
#endregion
+ ///
+ /// Fetches the next page of scores in the given direction.
+ ///
+ /// The direction, or null to fetch any scores.
+ private void fetchScores(int? direction)
+ {
+ Debug.Assert(direction == null || direction == -1 || direction == 1);
+
+ if (!lastFetchTask.IsCompleted)
+ return;
+
+ lastFetchTask = Task.Run(async () =>
+ {
+ ScoreInfo[] scores;
+
+ switch (direction)
+ {
+ default:
+ scores = await FetchScores().ConfigureAwait(false);
+ break;
+
+ case -1:
+ case 1:
+ scores = await FetchNextPage(direction.Value).ConfigureAwait(false);
+ break;
+ }
+
+ await addScores(scores).ConfigureAwait(false);
+ });
+ }
+
///
/// Performs a fetch/refresh of scores to be displayed.
///
From e48d36ad1edd2226b5e7afd9e3bc3e397d00d7e3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Wed, 26 Feb 2025 11:10:33 +0100
Subject: [PATCH 126/262] Add failing encoder test case for too many combo
colours
---
.../Formats/LegacyBeatmapEncoderTest.cs | 29 +++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index c8a09786ec..caebf52026 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -28,6 +28,7 @@ using osu.Game.Skinning;
using osu.Game.Storyboards;
using osu.Game.Tests.Resources;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Tests.Beatmaps.Formats
{
@@ -184,6 +185,32 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(5));
}
+ [Test]
+ public void TestOnlyEightComboColoursEncoded()
+ {
+ var beatmapSkin = new LegacyBeatmapSkin(new BeatmapInfo(), null)
+ {
+ Configuration =
+ {
+ CustomComboColours =
+ {
+ new Color4(1, 1, 1, 255),
+ new Color4(2, 2, 2, 255),
+ new Color4(3, 3, 3, 255),
+ new Color4(4, 4, 4, 255),
+ new Color4(5, 5, 5, 255),
+ new Color4(6, 6, 6, 255),
+ new Color4(7, 7, 7, 255),
+ new Color4(8, 8, 8, 255),
+ new Color4(9, 9, 9, 255),
+ }
+ }
+ };
+
+ var decodedAfterEncode = decodeFromLegacy(encodeToLegacy((new Beatmap(), beatmapSkin)), string.Empty);
+ Assert.That(decodedAfterEncode.skin.Configuration.CustomComboColours, Has.Count.EqualTo(8));
+ }
+
private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b)
{
// equal to null, no need to SequenceEqual
@@ -212,6 +239,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
{
var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader);
var beatmapSkin = new TestLegacySkin(beatmaps_resource_store, name);
+ stream.Seek(0, SeekOrigin.Begin);
+ beatmapSkin.Configuration = new LegacySkinDecoder().Decode(reader);
return (convert(beatmap), beatmapSkin);
}
}
From 2167c7b8d56bbba00a2167f093a1ddf77d09baf4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Wed, 26 Feb 2025 11:13:57 +0100
Subject: [PATCH 127/262] Limit beatmap encoder & decoder to at most 8 combo
colours
---
osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +-
osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index 07e88ab956..5529828de2 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -349,7 +349,7 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[Colours]");
- for (int i = 0; i < colours.Count; i++)
+ for (int i = 0; i < Math.Min(colours.Count, LegacyBeatmapDecoder.MAX_COMBO_COLOUR_COUNT); i++)
{
var comboColour = colours[i];
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index ca4fadf458..6c290c4f1c 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -18,6 +18,8 @@ namespace osu.Game.Beatmaps.Formats
{
public const int LATEST_VERSION = 14;
+ public const int MAX_COMBO_COLOUR_COUNT = 8;
+
///
/// The .osu format (beatmap) version.
///
@@ -126,7 +128,9 @@ namespace osu.Game.Beatmaps.Formats
string[] split = pair.Value.Split(',');
Color4 colour = convertSettingStringToColor4(split, allowAlpha, pair);
- bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal);
+ bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal)
+ && int.TryParse(pair.Key[5..], out int comboIndex)
+ && comboIndex >= 1 && comboIndex <= MAX_COMBO_COLOUR_COUNT;
if (isCombo)
{
From 6b76b8ccdda0ffe4a0b7d47e7fe3ddfd38e70d30 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Wed, 26 Feb 2025 11:16:37 +0100
Subject: [PATCH 128/262] Do not allow adding more than 8 combo colours in
editor
---
osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs | 10 ++++++----
osu.Game/Screens/Edit/Setup/ColoursSection.cs | 9 +++++++++
2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs
index fad58841e3..258a97d79c 100644
--- a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs
@@ -31,9 +31,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
public LocalisableString Caption { get; init; }
public LocalisableString HintText { get; init; }
+ public BindableBool CanAdd { get; } = new BindableBool(true);
+
private Box background = null!;
private FormFieldCaption caption = null!;
private FillFlowContainer flow = null!;
+ private RoundedButton addButton = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
@@ -47,8 +50,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
Masking = true;
CornerRadius = 5;
- RoundedButton button;
-
InternalChildren = new Drawable[]
{
background = new Box
@@ -76,7 +77,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Spacing = new Vector2(5),
- Child = button = new RoundedButton
+ Child = addButton = new RoundedButton
{
Action = addNewColour,
Size = new Vector2(70),
@@ -87,7 +88,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
},
};
- flow.SetLayoutPosition(button, float.MaxValue);
+ flow.SetLayoutPosition(addButton, float.MaxValue);
}
protected override void LoadComplete()
@@ -99,6 +100,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
if (args.Action != NotifyCollectionChangedAction.Replace)
updateColours();
}, true);
+ CanAdd.BindValueChanged(_ => addButton.Alpha = CanAdd.Value ? 1 : 0, true);
updateState();
}
diff --git a/osu.Game/Screens/Edit/Setup/ColoursSection.cs b/osu.Game/Screens/Edit/Setup/ColoursSection.cs
index 8de7f86523..865fe05c54 100644
--- a/osu.Game/Screens/Edit/Setup/ColoursSection.cs
+++ b/osu.Game/Screens/Edit/Setup/ColoursSection.cs
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
+using osu.Game.Beatmaps.Formats;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Skinning;
@@ -54,6 +55,8 @@ namespace osu.Game.Screens.Edit.Setup
Beatmap.BeatmapSkin.ComboColours.Clear();
Beatmap.BeatmapSkin.ComboColours.AddRange(comboColours.Colours);
+ updateAddButtonVisibility();
+
syncingColours = false;
}
});
@@ -68,8 +71,14 @@ namespace osu.Game.Screens.Edit.Setup
comboColours.Colours.Clear();
comboColours.Colours.AddRange(Beatmap.BeatmapSkin?.ComboColours);
+ updateAddButtonVisibility();
+
syncingColours = false;
});
+
+ updateAddButtonVisibility();
+
+ void updateAddButtonVisibility() => comboColours.CanAdd.Value = comboColours.Colours.Count < LegacyBeatmapDecoder.MAX_COMBO_COLOUR_COUNT;
}
}
}
From f3632a466fbf88484d2c3be9e461a9e7610e40da Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Wed, 26 Feb 2025 12:01:30 +0100
Subject: [PATCH 129/262] Prevent closing team chat channels via Ctrl-W
As pointed out in
https://github.com/ppy/osu/pull/32079#issuecomment-2680297760.
The comment suggested putting that logic in `ChannelManager` but
honestly I kinda don't see it working out. It'd probably be multiple
boolean arguments for `leaveChannel()` (because `sendLeaveRequest` or
whatever already exists), and then there's this one usage in tournament
client:
https://github.com/ppy/osu/blob/31aded69714cf205c215893368d1f148c9a73319/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs#L57-L58
I'm not sure how that would interact with this particular change, but I
think there is a nonzero possibility that it would interact badly. So in
general I kinda just prefer steering clear of all that and adding a
local one-liner.
---
osu.Game/Overlays/ChatOverlay.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs
index c49afa3a66..7f4ba3e2e2 100644
--- a/osu.Game/Overlays/ChatOverlay.cs
+++ b/osu.Game/Overlays/ChatOverlay.cs
@@ -228,7 +228,8 @@ namespace osu.Game.Overlays
return true;
case PlatformAction.DocumentClose:
- channelManager.LeaveChannel(currentChannel.Value);
+ if (currentChannel.Value?.Type != ChannelType.Team)
+ channelManager.LeaveChannel(currentChannel.Value);
return true;
case PlatformAction.TabRestore:
From d3c4afe65d8d86edb8c391d6db96849ef4f48709 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Thu, 27 Feb 2025 13:16:51 +0900
Subject: [PATCH 130/262] Fix typo
---
osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index 9bef6a3f3a..0190a6f959 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -71,7 +71,7 @@ namespace osu.Game.Screens.Ranking.Expanded
StarDifficulty starDifficulty = new StarDifficulty(beatmap.StarRating, 0);
// In some cases, the beatmap ferried through ScoreInfo actually represents an online beatmap.
- // If it isn't, we may be able to compute a more accuracy difficulty from the ruleset and mods.
+ // If it isn't, we may be able to compute a more accurate difficulty from the ruleset and mods.
if (realmAccess.Run(r => r.Find(score.BeatmapInfo!.ID)) != null)
starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods).GetResultSafely() ?? starDifficulty;
From d31588939c03fb365cf7acd09b6a441a49f100f7 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Thu, 27 Feb 2025 13:39:16 +0900
Subject: [PATCH 131/262] Disallow attempting to close multiplayer rooms
---
osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 10 +---------
.../Multiplayer/MultiplayerLoungeSubScreen.cs | 3 +++
.../OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs | 11 +++++++++++
3 files changed, 15 insertions(+), 9 deletions(-)
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
index 0e08e398a4..30e7b0d31b 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
@@ -21,7 +21,6 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets;
@@ -361,14 +360,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
api.Queue(req);
}
- public void Close(Room room)
- {
- Debug.Assert(room.RoomID != null);
-
- var request = new ClosePlaylistRequest(room.RoomID.Value);
- request.Success += RefreshRooms;
- api.Queue(request);
- }
+ public abstract void Close(Room room);
///
/// Push a room as a new subscreen.
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
index 873a9cde88..8f2490f77a 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
@@ -99,6 +99,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
});
}
+ public override void Close(Room room)
+ => throw new NotSupportedException("Cannot close multiplayer rooms.");
+
protected override void OpenNewRoom(Room room)
{
if (!client.IsConnected.Value)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
index 6ed367328c..9de13eb270 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
@@ -4,12 +4,14 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
@@ -74,6 +76,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
api.Queue(joinRoomRequest);
}
+ public override void Close(Room room)
+ {
+ Debug.Assert(room.RoomID != null);
+
+ var request = new ClosePlaylistRequest(room.RoomID.Value);
+ request.Success += RefreshRooms;
+ api.Queue(request);
+ }
+
protected override OsuButton CreateNewRoomButton() => new CreatePlaylistsRoomButton();
protected override Room CreateNewRoom()
From 47ca5c90a5bada5733c89376916236b29c69467f Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Thu, 27 Feb 2025 14:50:35 +0900
Subject: [PATCH 132/262] Refactor post-join setup to not pass delegates around
---
.../Online/Multiplayer/MultiplayerClient.cs | 77 +++++++++++--------
1 file changed, 47 insertions(+), 30 deletions(-)
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 636cba719b..1f85aa5d45 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -170,13 +170,23 @@ namespace osu.Game.Online.Multiplayer
private readonly TaskChain joinOrLeaveTaskChain = new TaskChain();
private CancellationTokenSource? joinCancellationSource;
+ ///
+ /// Creates and joins a described by an API .
+ ///
+ /// The API describing the room to create.
+ /// If the current user is already in another room.
public async Task CreateRoom(Room room)
{
if (Room != null)
throw new InvalidOperationException("Cannot create a multiplayer room while already in one.");
var cancellationSource = joinCancellationSource = new CancellationTokenSource();
- await initRoom(room, r => CreateRoomInternal(new MultiplayerRoom(room)), cancellationSource.Token).ConfigureAwait(false);
+
+ await joinOrLeaveTaskChain.Add(async () =>
+ {
+ var multiplayerRoom = await CreateRoomInternal(new MultiplayerRoom(room)).ConfigureAwait(false);
+ await setupJoinedRoom(room, multiplayerRoom, cancellationSource.Token).ConfigureAwait(false);
+ }, cancellationSource.Token).ConfigureAwait(false);
}
///
@@ -184,54 +194,61 @@ namespace osu.Game.Online.Multiplayer
///
/// The API .
/// An optional password to use for the join operation.
+ /// If the current user is already in another room, or does not represent an active room.
public async Task JoinRoom(Room room, string? password = null)
{
if (Room != null)
throw new InvalidOperationException("Cannot join a multiplayer room while already in one.");
- Debug.Assert(room.RoomID != null);
+ if (room.RoomID == null)
+ throw new InvalidOperationException("Cannot join an inactive room.");
var cancellationSource = joinCancellationSource = new CancellationTokenSource();
- await initRoom(room, r => JoinRoomInternal(room.RoomID.Value, password ?? room.Password), cancellationSource.Token).ConfigureAwait(false);
- }
- private async Task initRoom(Room room, Func> initFunc, CancellationToken cancellationToken)
- {
await joinOrLeaveTaskChain.Add(async () =>
{
- // Initialise the server-side room.
- MultiplayerRoom joinedRoom = await initFunc(room).ConfigureAwait(false);
+ var multiplayerRoom = await JoinRoomInternal(room.RoomID.Value, password ?? room.Password).ConfigureAwait(false);
+ await setupJoinedRoom(room, multiplayerRoom, cancellationSource.Token).ConfigureAwait(false);
+ }, cancellationSource.Token).ConfigureAwait(false);
+ }
- // Populate users.
- await PopulateUsers(joinedRoom.Users).ConfigureAwait(false);
+ ///
+ /// Performs post-join setup of a .
+ ///
+ /// The incoming API that was requested to be joined.
+ /// The resuling that was joined.
+ /// A token to cancel the process.
+ private async Task setupJoinedRoom(Room apiRoom, MultiplayerRoom joinedRoom, CancellationToken cancellationToken)
+ {
+ // Populate users.
+ await PopulateUsers(joinedRoom.Users).ConfigureAwait(false);
- // Update the stored room (must be done on update thread for thread-safety).
- await runOnUpdateThreadAsync(() =>
- {
- Debug.Assert(Room == null);
- Debug.Assert(APIRoom == null);
+ // Update the stored room (must be done on update thread for thread-safety).
+ await runOnUpdateThreadAsync(() =>
+ {
+ Debug.Assert(Room == null);
+ Debug.Assert(APIRoom == null);
- Room = joinedRoom;
- APIRoom = room;
+ Room = joinedRoom;
+ APIRoom = apiRoom;
- APIRoom.RoomID = joinedRoom.RoomID;
- APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray();
- APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
- // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client.
- APIRoom.EndDate = null;
+ APIRoom.RoomID = joinedRoom.RoomID;
+ APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray();
+ APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
+ // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client.
+ APIRoom.EndDate = null;
- Debug.Assert(LocalUser != null);
- addUserToAPIRoom(LocalUser);
+ Debug.Assert(LocalUser != null);
+ addUserToAPIRoom(LocalUser);
- foreach (var user in joinedRoom.Users)
- updateUserPlayingState(user.UserID, user.State);
+ foreach (var user in joinedRoom.Users)
+ updateUserPlayingState(user.UserID, user.State);
- updateLocalRoomSettings(joinedRoom.Settings);
+ updateLocalRoomSettings(joinedRoom.Settings);
- postServerShuttingDownNotification();
+ postServerShuttingDownNotification();
- OnRoomJoined();
- }, cancellationToken).ConfigureAwait(false);
+ OnRoomJoined();
}, cancellationToken).ConfigureAwait(false);
}
From 0b453772da964dddd2ee73f677367293b26dbf2a Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Thu, 27 Feb 2025 15:14:53 +0900
Subject: [PATCH 133/262] Disable button instead of hiding (and add tooltip)
---
.../Graphics/UserInterfaceV2/FormColourPalette.cs | 14 +++++++++++++-
osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs | 5 ++++-
.../Overlays/BeatmapSet/Buttons/FavouriteButton.cs | 5 ++---
osu.Game/Overlays/Settings/SettingsButton.cs | 5 +----
.../Screens/OnlinePlay/Components/ReadyButton.cs | 5 ++---
.../Playlists/AddPlaylistToCollectionButton.cs | 5 ++---
6 files changed, 24 insertions(+), 15 deletions(-)
diff --git a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs
index 258a97d79c..a0348fa27a 100644
--- a/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs
@@ -100,7 +100,19 @@ namespace osu.Game.Graphics.UserInterfaceV2
if (args.Action != NotifyCollectionChangedAction.Replace)
updateColours();
}, true);
- CanAdd.BindValueChanged(_ => addButton.Alpha = CanAdd.Value ? 1 : 0, true);
+ CanAdd.BindValueChanged(canAdd =>
+ {
+ if (canAdd.NewValue)
+ {
+ addButton.Enabled.Value = true;
+ addButton.TooltipText = string.Empty;
+ }
+ else
+ {
+ addButton.Enabled.Value = false;
+ addButton.TooltipText = "Maximum combo colours reached";
+ }
+ }, true);
updateState();
}
diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
index 6aded3fe32..9b57ebb200 100644
--- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs
@@ -8,6 +8,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Backgrounds;
@@ -17,7 +18,7 @@ using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2
{
- public partial class RoundedButton : OsuButton, IFilterable
+ public partial class RoundedButton : OsuButton, IFilterable, IHasTooltip
{
protected TrianglesV2? Triangles { get; private set; }
@@ -107,5 +108,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
public bool FilteringActive { get; set; }
+
+ public virtual LocalisableString TooltipText { get; set; }
}
}
diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs
index cbdb2ea190..eab394c8f6 100644
--- a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs
+++ b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs
@@ -7,7 +7,6 @@ using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
@@ -21,7 +20,7 @@ using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Overlays.BeatmapSet.Buttons
{
- public partial class FavouriteButton : HeaderButton, IHasTooltip
+ public partial class FavouriteButton : HeaderButton
{
public readonly Bindable BeatmapSet = new Bindable();
@@ -32,7 +31,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
private readonly IBindable localUser = new Bindable();
- public LocalisableString TooltipText
+ public override LocalisableString TooltipText
{
get
{
diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs
index 3f5d612eb8..196ddca953 100644
--- a/osu.Game/Overlays/Settings/SettingsButton.cs
+++ b/osu.Game/Overlays/Settings/SettingsButton.cs
@@ -6,13 +6,12 @@ using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Overlays.Settings
{
- public partial class SettingsButton : RoundedButton, IHasTooltip, IConditionalFilterable
+ public partial class SettingsButton : RoundedButton, IConditionalFilterable
{
public SettingsButton()
{
@@ -25,8 +24,6 @@ namespace osu.Game.Overlays.Settings
public BindableBool CanBeShown { get; } = new BindableBool(true);
IBindable IConditionalFilterable.CanBeShown => CanBeShown;
- public LocalisableString TooltipText { get; set; }
-
public override IEnumerable FilterTerms
{
get
diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs
index 2e669fd1b2..56e2719e9c 100644
--- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs
@@ -3,7 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online;
@@ -11,7 +10,7 @@ using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Components
{
- public abstract partial class ReadyButton : RoundedButton, IHasTooltip
+ public abstract partial class ReadyButton : RoundedButton
{
public new readonly BindableBool Enabled = new BindableBool();
@@ -29,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
private void updateState() =>
base.Enabled.Value = availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value;
- public virtual LocalisableString TooltipText
+ public override LocalisableString TooltipText
{
get
{
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs
index 741173f9a3..47629981f1 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/AddPlaylistToCollectionButton.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Collections;
@@ -18,7 +17,7 @@ using Realms;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
- public partial class AddPlaylistToCollectionButton : RoundedButton, IHasTooltip
+ public partial class AddPlaylistToCollectionButton : RoundedButton
{
private readonly Room room;
@@ -161,7 +160,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
collectionSubscription?.Dispose();
}
- public LocalisableString TooltipText
+ public override LocalisableString TooltipText
{
get
{
From 5b318edbfbd9aa3ece3a491a9a641d7eee3a4c20 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Thu, 27 Feb 2025 14:57:42 +0100
Subject: [PATCH 134/262] Fix sliders not being selectable if the body is
hidden but the head is still visible
Closes https://github.com/ppy/osu/issues/31998.
Previously: https://github.com/ppy/osu/commit/1648f2efa306f587714178f113e69d8ad8c4ac02,
https://github.com/ppy/osu/pull/31923.
Oh input handling, how I love ya.
---
.../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 39c0681dba..60f335c419 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -626,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
- if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0))
+ if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0 || DrawableObject.HeadCircle.Alpha > 0))
return true;
if (ControlPointVisualiser == null)
From 09131740992b15ca322054e5c8aee784c6eade79 Mon Sep 17 00:00:00 2001
From: Zihad
Date: Fri, 28 Feb 2025 00:20:58 +0600
Subject: [PATCH 135/262] Fix settings control not visible because of previous
search
This also makes `SettingsPanel`'s `SearchTextBox` protected from private so that `SettingsOverlay` can access it.
---
osu.Game/Overlays/SettingsOverlay.cs | 3 +++
osu.Game/Overlays/SettingsPanel.cs | 16 ++++++++--------
2 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs
index 1157860e03..8a39d75565 100644
--- a/osu.Game/Overlays/SettingsOverlay.cs
+++ b/osu.Game/Overlays/SettingsOverlay.cs
@@ -68,6 +68,9 @@ namespace osu.Game.Overlays
public void ShowAtControl()
where T : Drawable
{
+ // if search isn't cleared then the target control won't be visible if it doesn't match the query
+ SearchTextBox.Current.Value = "";
+
Show();
// wait for load of sections
diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs
index df50e0f339..d8b054eaf8 100644
--- a/osu.Game/Overlays/SettingsPanel.cs
+++ b/osu.Game/Overlays/SettingsPanel.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Overlays
public SettingsSectionsContainer SectionsContainer { get; private set; }
- private SeekLimitedSearchTextBox searchTextBox;
+ protected SeekLimitedSearchTextBox SearchTextBox;
protected override string PopInSampleName => "UI/settings-pop-in";
protected override double PopInOutSampleBalance => -OsuGameBase.SFX_STEREO_STRENGTH;
@@ -135,7 +135,7 @@ namespace osu.Game.Overlays
},
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
- Child = searchTextBox = new SettingsSearchTextBox
+ Child = SearchTextBox = new SettingsSearchTextBox
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.TopCentre,
@@ -183,8 +183,8 @@ namespace osu.Game.Overlays
Sidebar?.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint);
this.FadeTo(1, TRANSITION_LENGTH / 2, Easing.OutQuint);
- searchTextBox.TakeFocus();
- searchTextBox.HoldFocus = true;
+ SearchTextBox.TakeFocus();
+ SearchTextBox.HoldFocus = true;
}
protected virtual float ExpandedPosition => 0;
@@ -199,8 +199,8 @@ namespace osu.Game.Overlays
Sidebar?.MoveToX(-sidebar_width, TRANSITION_LENGTH, Easing.OutQuint);
this.FadeTo(0, TRANSITION_LENGTH / 2, Easing.OutQuint);
- searchTextBox.HoldFocus = false;
- if (searchTextBox.HasFocus)
+ SearchTextBox.HoldFocus = false;
+ if (SearchTextBox.HasFocus)
GetContainingFocusManager()!.ChangeFocus(null);
}
@@ -208,7 +208,7 @@ namespace osu.Game.Overlays
protected override void OnFocus(FocusEvent e)
{
- searchTextBox.TakeFocus();
+ SearchTextBox.TakeFocus();
base.OnFocus(e);
}
@@ -234,7 +234,7 @@ namespace osu.Game.Overlays
loading.Hide();
- searchTextBox.Current.BindValueChanged(term => SectionsContainer.SearchTerm = term.NewValue, true);
+ SearchTextBox.Current.BindValueChanged(term => SectionsContainer.SearchTerm = term.NewValue, true);
loadSidebarButtons();
});
From a659936c57a1f51b917102bc737bfbc22187973e Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 28 Feb 2025 13:19:19 +0900
Subject: [PATCH 136/262] Inline some methods
---
.../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 4 +---
.../OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs | 8 ++------
2 files changed, 3 insertions(+), 9 deletions(-)
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
index eda3bace40..f74de26f1f 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
@@ -444,7 +444,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
if (!ApplyButton.Enabled.Value)
return;
- hideError();
+ ErrorText.FadeOut(50);
Debug.Assert(applyingSettingsOperation == null);
applyingSettingsOperation = ongoingOperationTracker.BeginOperation();
@@ -480,8 +480,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}
}
- private void hideError() => ErrorText.FadeOut(50);
-
private void onSuccess() => Schedule(() =>
{
Debug.Assert(applyingSettingsOperation != null);
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
index b3d1d577ed..9c0363f40e 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs
@@ -437,7 +437,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
if (!ApplyButton.Enabled.Value)
return;
- hideError();
+ ErrorText.FadeOut(50);
room.Name = NameField.Text;
room.Availability = AvailabilityPicker.Current.Value;
@@ -448,15 +448,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
loadingLayer.Show();
var req = new CreateRoomRequest(room);
- req.Success += onSuccess;
+ req.Success += _ => loadingLayer.Hide();
req.Failure += e => onError(req.Response?.Error ?? e.Message);
api.Queue(req);
}
- private void hideError() => ErrorText.FadeOut(50);
-
- private void onSuccess(Room room) => loadingLayer.Hide();
-
private void onError(string text)
{
// see https://github.com/ppy/osu-web/blob/2c97aaeb64fb4ed97c747d8383a35b30f57428c7/app/Models/Multiplayer/PlaylistItem.php#L48.
From e1723ec1bbfe40e70754b1971b9e1602eed4a7a5 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 28 Feb 2025 14:05:49 +0900
Subject: [PATCH 137/262] Adjust preview time display to not conflict with
bookmarks
---
.../Timelines/Summary/Parts/PreviewTimePart.cs | 5 +++++
.../Components/Timelines/Summary/SummaryTimeline.cs | 13 ++++++-------
2 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs
index 67bb1ef500..72b58bcb5f 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/PreviewTimePart.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Extensions;
@@ -36,6 +37,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
: base(time)
{
Alpha = 0.8f;
+
+ // Display as a small circle on the middle line as to not clash with other displays.
+ RelativeSizeAxes = Axes.None;
+ Height = Width = 5;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs
index c01481e840..568137cce1 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs
@@ -52,13 +52,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
},
}
},
- new PreviewTimePart
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.TopCentre,
- RelativeSizeAxes = Axes.Both,
- Height = 0.4f,
- },
new BreakPart
{
Anchor = Anchor.Centre,
@@ -85,6 +78,12 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
RelativeSizeAxes = Axes.Both,
Height = 0.4f
},
+ new PreviewTimePart
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ },
new MarkerPart { RelativeSizeAxes = Axes.Both },
};
}
From 3e8dafa3c51d6c6434d56ac0c51ffe4800c23fd4 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 28 Feb 2025 14:43:00 +0900
Subject: [PATCH 138/262] Add basic setup for mania legacy barline
implementation
---
.../Objects/Drawables/DrawableBarLine.cs | 3 +-
.../Skinning/Default/DefaultBarLine.cs | 4 ++-
.../Skinning/Legacy/LegacyBarLine.cs | 33 +++++++++++++++++++
.../Legacy/ManiaLegacySkinTransformer.cs | 2 +-
4 files changed, 38 insertions(+), 4 deletions(-)
create mode 100644 osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
index 25fed1a84c..be0f84d7fd 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
@@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
: base(barLine)
{
RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
}
[BackgroundDependencyLoader]
@@ -36,8 +37,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
-
- Major.BindValueChanged(major => Height = major.NewValue ? 1.7f : 1.2f, true);
}
protected override void OnApply()
diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs
index ef75e9df11..05fba1241f 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject)
{
- RelativeSizeAxes = Axes.Both;
+ RelativeSizeAxes = Axes.X;
// Avoid flickering due to no anti-aliasing of boxes by default.
var edgeSmoothness = new Vector2(0.3f);
@@ -75,6 +75,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default
private void updateMajor(ValueChangedEvent major)
{
+ Height = major.NewValue ? 1.7f : 1.2f;
+
mainLine.Alpha = major.NewValue ? 0.5f : 0.2f;
leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? mainLine.Alpha * 0.3f : 0;
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs
new file mode 100644
index 0000000000..64ea1df2ae
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.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 osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Legacy
+{
+ public partial class LegacyBarLine : CompositeDrawable
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = 1.2f;
+
+ // Avoid flickering due to no anti-aliasing of boxes by default.
+ var edgeSmoothness = new Vector2(0.3f);
+
+ AddInternal(new Box
+ {
+ Name = "Bar line",
+ EdgeSmoothness = edgeSmoothness,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.Both,
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
index 76af569b95..c321fcda87 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
@@ -163,7 +163,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return new LegacyStageForeground();
case ManiaSkinComponents.BarLine:
- return null; // Not yet implemented.
+ return new LegacyBarLine();
default:
throw new UnsupportedSkinComponentException(lookup);
From cb29459a1e5c2d97a68a548c592ea3140513632d Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 28 Feb 2025 15:13:13 +0900
Subject: [PATCH 139/262] Add support for legacy osu!mania barline height and
colour spec
---
.../Objects/Drawables/DrawableBarLine.cs | 4 ++--
osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs | 9 +++++++--
osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 1 +
osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs | 3 +++
osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 4 ++++
osu.Game/Skinning/LegacySkin.cs | 6 ++++++
6 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
index be0f84d7fd..c9fc0763a8 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
@@ -26,10 +26,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
: base(barLine)
{
RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
+ Height = 1;
}
- [BackgroundDependencyLoader]
+ [BackgroundDependencyLoader(true)]
private void load()
{
AddInternal(new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.BarLine), _ => new DefaultBarLine())
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs
index 64ea1df2ae..ce48c49b2e 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBarLine.cs
@@ -5,17 +5,22 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Skinning;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public partial class LegacyBarLine : CompositeDrawable
{
[BackgroundDependencyLoader]
- private void load()
+ private void load(ISkinSource skin)
{
+ float skinHeight = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.BarLineHeight)?.Value ?? 1;
+
RelativeSizeAxes = Axes.X;
- Height = 1.2f;
+ Height = 1.2f * skinHeight;
+ Colour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.BarLineColour)?.Value ?? Color4.White;
// Avoid flickering due to no anti-aliasing of boxes by default.
var edgeSmoothness = new Vector2(0.3f);
diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs
index db1f216b6e..1e6fa44e68 100644
--- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs
+++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs
@@ -41,6 +41,7 @@ namespace osu.Game.Skinning
public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR;
public float ComboPosition = 111 * POSITION_SCALE_FACTOR;
public float ScorePosition = 300 * POSITION_SCALE_FACTOR;
+ public float BarLineHeight = 1;
public bool ShowJudgementLine = true;
public bool KeysUnderNotes;
public int LightFramePerSecond = 60;
diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs
index ee354de68b..e94fb23681 100644
--- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs
+++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs
@@ -70,6 +70,9 @@ namespace osu.Game.Skinning
RightStageImage,
BottomStageImage,
+ BarLineHeight,
+ BarLineColour,
+
// ReSharper disable once InconsistentNaming
Hit300g,
diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
index 09866ef237..2739743387 100644
--- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
+++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
@@ -86,6 +86,10 @@ namespace osu.Game.Skinning
parseArrayValue(pair.Value, currentConfig.ColumnWidth);
break;
+ case "BarlineHeight":
+ currentConfig.BarLineHeight = float.Parse(pair.Value, CultureInfo.InvariantCulture);
+ break;
+
case "HitPosition":
currentConfig.HitPosition = (480 - Math.Clamp(float.Parse(pair.Value, CultureInfo.InvariantCulture), 240, 480)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR;
break;
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 08fa068830..51c1473303 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -198,9 +198,15 @@ namespace osu.Game.Skinning
case LegacyManiaSkinConfigurationLookups.ComboBreakColour:
return SkinUtils.As(getCustomColour(existing, "ColourBreak"));
+ case LegacyManiaSkinConfigurationLookups.BarLineColour:
+ return SkinUtils.As(getCustomColour(existing, "ColourBarline"));
+
case LegacyManiaSkinConfigurationLookups.MinimumColumnWidth:
return SkinUtils.As(new Bindable(existing.MinimumColumnWidth));
+ case LegacyManiaSkinConfigurationLookups.BarLineHeight:
+ return SkinUtils.As(new Bindable(existing.BarLineHeight));
+
case LegacyManiaSkinConfigurationLookups.NoteBodyStyle:
if (existing.NoteBodyStyle != null)
From 306b30cb12238b48e2259d4611185821701d34a9 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Fri, 28 Feb 2025 15:51:54 +0900
Subject: [PATCH 140/262] Add failing test
---
.../TestSceneMultiplayerMatchSubScreen.cs | 23 +++++++++++++++++++
.../OnlinePlay/Match/DrawableMatchRoom.cs | 9 ++++----
2 files changed, 28 insertions(+), 4 deletions(-)
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
index e95209f993..7058532196 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
@@ -317,6 +317,29 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("score multiplier = 1.20", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
}
+ [Test]
+ public void TestChangeSettingsButtonVisibleForHost()
+ {
+ AddStep("add playlist item", () =>
+ {
+ SelectedRoom.Value!.Playlist =
+ [
+ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
+ {
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID
+ }
+ ];
+ });
+ ClickButtonWhenEnabled();
+
+ AddUntilStep("wait for join", () => RoomJoined);
+
+ AddUntilStep("button visible", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.GreaterThan(0));
+ AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }));
+ AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID));
+ AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.EqualTo(0));
+ }
+
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen
{
[Resolved(canBeNull: true)]
diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs
index 08bcf32edf..b10e83a05c 100644
--- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs
@@ -25,12 +25,13 @@ namespace osu.Game.Screens.OnlinePlay.Match
set => selectedItem.Current = value;
}
+ public Drawable? ChangeSettingsButton { get; private set; }
+
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly BindableWithCurrent selectedItem = new BindableWithCurrent();
private readonly bool allowEdit;
- private Drawable? editButton;
public DrawableMatchRoom(Room room, bool allowEdit = true)
: base(room)
@@ -45,7 +46,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
{
if (allowEdit)
{
- ButtonsContainer.Add(editButton = new PurpleRoundedButton
+ ButtonsContainer.Add(ChangeSettingsButton = new PurpleRoundedButton
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
@@ -73,8 +74,8 @@ namespace osu.Game.Screens.OnlinePlay.Match
private void updateRoomHost()
{
- if (editButton != null)
- editButton.Alpha = Room.Host?.Equals(api.LocalUser.Value) == true ? 1 : 0;
+ if (ChangeSettingsButton != null)
+ ChangeSettingsButton.Alpha = Room.Host?.Equals(api.LocalUser.Value) == true ? 1 : 0;
}
protected override UpdateableBeatmapBackgroundSprite CreateBackground() => base.CreateBackground().With(d =>
From a09ef5d96d0bcd9c56ccd1eb6747fa5ba6d0e449 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Fri, 28 Feb 2025 15:52:02 +0900
Subject: [PATCH 141/262] Fix API room host not being populated
---
osu.Game/Online/Multiplayer/MultiplayerClient.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 1f85aa5d45..3c627c7a47 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -222,6 +222,8 @@ namespace osu.Game.Online.Multiplayer
{
// Populate users.
await PopulateUsers(joinedRoom.Users).ConfigureAwait(false);
+ if (joinedRoom.Host != null)
+ await PopulateUsers([joinedRoom.Host]).ConfigureAwait(false);
// Update the stored room (must be done on update thread for thread-safety).
await runOnUpdateThreadAsync(() =>
@@ -233,6 +235,7 @@ namespace osu.Game.Online.Multiplayer
APIRoom = apiRoom;
APIRoom.RoomID = joinedRoom.RoomID;
+ APIRoom.Host = joinedRoom.Host?.User;
APIRoom.Playlist = joinedRoom.Playlist.Select(item => new PlaylistItem(item)).ToArray();
APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
// The server will null out the end date upon the host joining the room, but the null value is never communicated to the client.
From 02b950223c055aad3e192cdff99d56f2c5b2c83f Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 28 Feb 2025 16:06:12 +0900
Subject: [PATCH 142/262] Adjust x offsets to work again for keyboard selection
---
osu.Game/Screens/SelectV2/PanelBase.cs | 13 ++++++-------
osu.Game/Screens/SelectV2/PanelBeatmap.cs | 2 --
2 files changed, 6 insertions(+), 9 deletions(-)
diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs
index 805cbac8eb..1e47401013 100644
--- a/osu.Game/Screens/SelectV2/PanelBase.cs
+++ b/osu.Game/Screens/SelectV2/PanelBase.cs
@@ -23,8 +23,6 @@ namespace osu.Game.Screens.SelectV2
{
private const float corner_radius = 10;
- private const float left_edge_x_offset = 20f;
- private const float keyboard_active_x_offset = 25f;
private const float active_x_offset = 50f;
private const float duration = 500;
@@ -162,6 +160,7 @@ namespace osu.Game.Screens.SelectV2
base.LoadComplete();
Expanded.BindValueChanged(_ => updateDisplay());
+ Selected.BindValueChanged(_ => updateDisplay());
KeyboardSelected.BindValueChanged(_ => updateDisplay(), true);
}
@@ -199,13 +198,13 @@ namespace osu.Game.Screens.SelectV2
private void updateXOffset()
{
- float x = PanelXOffset + active_x_offset + keyboard_active_x_offset + left_edge_x_offset;
+ float x = PanelXOffset;
- if (Expanded.Value)
- x -= active_x_offset;
+ if (!Expanded.Value && !Selected.Value)
+ x += active_x_offset;
- if (KeyboardSelected.Value)
- x -= keyboard_active_x_offset;
+ if (!KeyboardSelected.Value)
+ x += active_x_offset * 0.5f;
this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint);
}
diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs
index b27e5cae14..0ce6b1a9a2 100644
--- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs
+++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs
@@ -163,8 +163,6 @@ namespace osu.Game.Screens.SelectV2
computeStarRating();
updateKeyCount();
}, true);
-
- Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true);
}
protected override void PrepareForUse()
From a8fbac0f0dbf628ee284e9b3c27554d00697f1e8 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 28 Feb 2025 16:27:18 +0900
Subject: [PATCH 143/262] Add better selection visibility via another tint
layer
---
osu.Game/Screens/SelectV2/PanelBase.cs | 53 +++++++++++++++++++++-----
1 file changed, 44 insertions(+), 9 deletions(-)
diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs
index 1e47401013..d3132a106e 100644
--- a/osu.Game/Screens/SelectV2/PanelBase.cs
+++ b/osu.Game/Screens/SelectV2/PanelBase.cs
@@ -38,6 +38,8 @@ namespace osu.Game.Screens.SelectV2
private Container iconContainer = null!;
private Box activationFlash = null!;
private Box hoverLayer = null!;
+ private Box keyboardSelectionLayer = null!;
+ private Box selectionLayer = null!;
public Container TopLevelContent { get; private set; } = null!;
@@ -137,6 +139,24 @@ namespace osu.Game.Screens.SelectV2
hoverLayer = new Box
{
Alpha = 0,
+ Colour = colours.Blue.Opacity(0.1f),
+ Blending = BlendingParameters.Additive,
+ RelativeSizeAxes = Axes.Both,
+ },
+ selectionLayer = new Box
+ {
+ Alpha = 0,
+ Colour = ColourInfo.GradientHorizontal(colours.Yellow.Opacity(0), colours.Yellow.Opacity(0.5f)),
+ Blending = BlendingParameters.Additive,
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.7f,
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ },
+ keyboardSelectionLayer = new Box
+ {
+ Alpha = 0,
+ Colour = colours.Yellow.Opacity(0.1f),
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
},
@@ -151,7 +171,6 @@ namespace osu.Game.Screens.SelectV2
}
};
- hoverLayer.Colour = colours.Blue.Opacity(0.1f);
backgroundGradient.Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4);
}
@@ -159,9 +178,27 @@ namespace osu.Game.Screens.SelectV2
{
base.LoadComplete();
- Expanded.BindValueChanged(_ => updateDisplay());
- Selected.BindValueChanged(_ => updateDisplay());
- KeyboardSelected.BindValueChanged(_ => updateDisplay(), true);
+ Expanded.BindValueChanged(_ => updateDisplay(), true);
+
+ Selected.BindValueChanged(selected =>
+ {
+ if (selected.NewValue)
+ selectionLayer.FadeIn(100, Easing.OutQuint);
+ else
+ selectionLayer.FadeOut(200, Easing.OutQuint);
+
+ updateXOffset();
+ }, true);
+
+ KeyboardSelected.BindValueChanged(selected =>
+ {
+ if (selected.NewValue)
+ keyboardSelectionLayer.FadeIn(100, Easing.OutQuint);
+ else
+ keyboardSelectionLayer.FadeOut(1000, Easing.OutQuint);
+
+ updateXOffset();
+ }, true);
}
protected override void PrepareForUse()
@@ -211,9 +248,7 @@ namespace osu.Game.Screens.SelectV2
private void updateHover()
{
- bool hovered = IsHovered || KeyboardSelected.Value;
-
- if (hovered)
+ if (IsHovered)
hoverLayer.FadeIn(100, Easing.OutQuint);
else
hoverLayer.FadeOut(1000, Easing.OutQuint);
@@ -221,13 +256,13 @@ namespace osu.Game.Screens.SelectV2
protected override bool OnHover(HoverEvent e)
{
- updateDisplay();
+ updateHover();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
- updateDisplay();
+ updateHover();
base.OnHoverLost(e);
}
From 1e46dc6b0a23cf2fa9677104b9101d8f3f94a18d Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 28 Feb 2025 16:27:42 +0900
Subject: [PATCH 144/262] Adjust animation duration to roughly match scroll
operations
Previous value felt wrong when using keyboard selection for iteration.
---
osu.Game/Screens/SelectV2/PanelBase.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs
index d3132a106e..2a32b1a95f 100644
--- a/osu.Game/Screens/SelectV2/PanelBase.cs
+++ b/osu.Game/Screens/SelectV2/PanelBase.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Screens.SelectV2
private const float active_x_offset = 50f;
- private const float duration = 500;
+ private const float duration = 400;
protected float PanelXOffset { get; init; }
From 51cb0bea1ce61ffd3ca8b3bdb641f8f4840601d1 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 28 Feb 2025 16:45:49 +0900
Subject: [PATCH 145/262] Fix carousel taking up too much space on new song
select implementation
---
osu.Game/Screens/SelectV2/SongSelectV2.cs | 29 +++++++++++++++++------
1 file changed, 22 insertions(+), 7 deletions(-)
diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelectV2.cs
index 3943d059f9..23139c8742 100644
--- a/osu.Game/Screens/SelectV2/SongSelectV2.cs
+++ b/osu.Game/Screens/SelectV2/SongSelectV2.cs
@@ -39,17 +39,32 @@ namespace osu.Game.Screens.SelectV2
{
AddRangeInternal(new Drawable[]
{
- new Container
+ new GridContainer // used for max width implementation
{
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT },
- Child = new BeatmapCarousel
+ ColumnDimensions = new[]
{
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- RelativeSizeAxes = Axes.Both,
- Width = 0.6f,
+ new Dimension(),
+ new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 750),
},
+ Content = new[]
+ {
+ new[]
+ {
+ Empty(),
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT },
+ Child = new BeatmapCarousel
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ },
+ }
+ }
},
modSelectOverlay,
});
From 0e257038e8b49400f5082570d5867c4c7ef23c3b Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 28 Feb 2025 16:47:57 +0900
Subject: [PATCH 146/262] Fix status pills displaying wrong
---
osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs
index 599d1b380a..7b99ad40de 100644
--- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs
+++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs
@@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps.Drawables
{
if (Status == BeatmapOnlineStatus.None)
{
- this.FadeOut(animation_duration, Easing.OutQuint);
+ Hide();
return;
}
From 8fc744e9dc7d0045232a6c1eda3c17160c366947 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 28 Feb 2025 17:55:11 +0900
Subject: [PATCH 147/262] Make `TestSceneSongSelect` work with local database
It was pointless before.
---
.../SongSelectV2/TestSceneSongSelect.cs | 36 ++-----------------
1 file changed, 3 insertions(+), 33 deletions(-)
diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs
index 33474d7449..6d180c76d9 100644
--- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs
@@ -9,16 +9,10 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
-using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
-using osu.Game.Beatmaps;
-using osu.Game.Configuration;
using osu.Game.Database;
-using osu.Game.Online.API;
-using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
-using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
@@ -29,7 +23,6 @@ using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu;
using osu.Game.Screens.SelectV2.Footer;
-using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelectV2
@@ -42,8 +35,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
[Cached]
private readonly OsuLogo logo;
- private BeatmapManager beatmapManager = null!;
-
protected override bool UseOnlineAPI => true;
public TestSceneSongSelect()
@@ -66,32 +57,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
}
[BackgroundDependencyLoader]
- private void load(GameHost host, IAPIProvider onlineAPI)
+ private void load()
{
- BeatmapStore beatmapStore;
- BeatmapUpdater beatmapUpdater;
- BeatmapDifficultyCache difficultyCache;
+ RealmDetachedBeatmapStore beatmapStore;
- // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install.
- // At a point we have isolated interactive test runs enough, this can likely be removed.
- Dependencies.Cache(new RealmRulesetStore(Realm));
- Dependencies.Cache(Realm);
- Dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
- Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, onlineAPI, Audio, Resources, host, Beatmap.Default, difficultyCache));
- Dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(beatmapManager, difficultyCache, onlineAPI, LocalStorage));
- Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
-
- beatmapManager.ProcessBeatmap = (set, scope) => beatmapUpdater.Process(set, scope);
-
- MusicController music;
- Dependencies.Cache(music = new MusicController());
-
- // required to get bindables attached
- Add(difficultyCache);
- Add(music);
+ Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
Add(beatmapStore);
-
- Dependencies.Cache(new OsuConfigManager(LocalStorage));
}
protected override void LoadComplete()
@@ -109,7 +80,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SongSelectV2()));
AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelectV2 songSelect && songSelect.IsLoaded);
- AddStep("import test beatmap", () => beatmapManager.Import(TestResources.GetTestBeatmapForImport()));
}
[Test]
From 993473c0810e55ce0b1143f0f147e88d10c65396 Mon Sep 17 00:00:00 2001
From: Dan Balasescu
Date: Fri, 28 Feb 2025 18:40:54 +0900
Subject: [PATCH 148/262] Pass through artist/title in beatmap transform
---
.../Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
index 572bf535f7..184de2f50c 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistItemResultsScreen.cs
@@ -223,6 +223,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Difficulty = new BeatmapDifficulty(beatmap.Difficulty),
Metadata =
{
+ Artist = beatmap.Metadata.Artist,
+ Title = beatmap.Metadata.Title,
Author = new RealmUser
{
Username = beatmap.Metadata.Author.Username,
From ffef6ae1853d84120abf52f3c93382b4863bd556 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Fri, 28 Feb 2025 13:34:00 +0100
Subject: [PATCH 149/262] Fix possible crash when scaling objects in editor
The specific fail case here is when `s.{X,Y}` is 0, and
`s{Lower,Upper}Bound` is `Infinity`. Because IEEE math is IEEE math,
`0 * Infinity` is `NaN`.
`MathHelper.Clamp()` is written the following way:
https://github.com/ppy/osuTK/blob/af742f1afd01828efc7bc9fe77536b54aab8b419/src/osuTK/Math/MathHelper.cs#L284-L306
`Math.{Min,Max}` are both documented as reporting `NaN` when any of
their operands are `NaN`:
https://learn.microsoft.com/en-us/dotnet/api/system.math.min?view=net-8.0#system-math-min(system-single-system-single)
https://learn.microsoft.com/en-us/dotnet/api/system.math.max?view=net-8.0#system-math-max(system-single-system-single)
which means that if a `NaN` happens to sneak into the bounds, it will
start spreading outwards in an uncontrolled manner, and likely crash
things.
In contrast, the standard library provided `Math.Clamp()` is written
like so:
https://github.com/dotnet/runtime/blob/577c36cee56480dec4d4610b35605b5d5836888b/src/libraries/System.Private.CoreLib/src/System/Math.cs#L711-L729
With this implementation, if either bound is `NaN`, it will essentially
not be checked (because any and all comparisons involving `NaN` return
false). This prevents the spread of `NaN`s, all the way to positions
of hitobjects, and thus fixes the crash.
---
.../Edit/OsuSelectionScaleHandler.cs | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
index e3ab95c402..4c3db207f2 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
@@ -263,12 +263,12 @@ namespace osu.Game.Rulesets.Osu.Edit
{
case Axes.X:
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - b, upperBounds - b, a);
- s.X = MathHelper.Clamp(s.X, sLowerBound, sUpperBound);
+ s.X = Math.Clamp(s.X, sLowerBound, sUpperBound);
break;
case Axes.Y:
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - a, upperBounds - a, b);
- s.Y = MathHelper.Clamp(s.Y, sLowerBound, sUpperBound);
+ s.Y = Math.Clamp(s.Y, sLowerBound, sUpperBound);
break;
case Axes.Both:
@@ -276,11 +276,11 @@ namespace osu.Game.Rulesets.Osu.Edit
// Therefore the ratio s.X / s.Y will be maintained
(sLowerBound, sUpperBound) = computeBounds(lowerBounds, upperBounds, a * s.X + b * s.Y);
s.X = s.X < 0
- ? MathHelper.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound)
- : MathHelper.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound);
+ ? Math.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound)
+ : Math.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound);
s.Y = s.Y < 0
- ? MathHelper.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound)
- : MathHelper.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound);
+ ? Math.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound)
+ : Math.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound);
break;
}
From 35b0ff80bb6094a32d9c5c2b93203faf491b68fd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?=
Date: Fri, 28 Feb 2025 13:41:56 +0100
Subject: [PATCH 150/262] Mark `MathHelper.Clamp()` as banned API
See previous commit for partial rationale.
There's an argument to be made about the `NaN`-spreading semantics being
desirable because at least something will loudly fail in that case, but
I'm not so sure about that these days. It feels like either way if
`NaN`s are produced, then things are outside of any control, and chances
are the game can probably continue without crashing. And, this move
reduces our dependence on osuTK, which has already been living on
borrowed time for years now and is only awaiting someone brave to go
excise it.
---
CodeAnalysis/BannedSymbols.txt | 3 +++
.../Beatmaps/PippidonBeatmapConverter.cs | 4 ++--
.../Skinning/Argon/ArgonBananaPiece.cs | 3 ++-
.../HitCircles/Components/HitCircleOverlapMarker.cs | 3 ++-
.../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 4 ++--
osu.Game/Overlays/NotificationOverlayToastTray.cs | 2 +-
osu.Game/Screens/Play/HUDOverlay.cs | 6 +++---
osu.Game/Screens/Utility/CircleGameplay.cs | 4 ++--
.../Utility/SampleComponents/LatencyMovableBox.cs | 9 +++++----
osu.Game/Screens/Utility/ScrollingGameplay.cs | 2 +-
10 files changed, 23 insertions(+), 17 deletions(-)
diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index 550f7c8e11..08b79fc2c0 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -18,3 +18,6 @@ M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize(
M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead.
M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead.
M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToKebabCase() instead.
+M:osuTK.MathHelper.Clamp(System.Int32,System.Int32,System.Int32)~System.Int32;Use Math.Clamp() instead.
+M:osuTK.MathHelper.Clamp(System.Single,System.Single,System.Single)~System.Single;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
+M:osuTK.MathHelper.Clamp(System.Double,System.Double,System.Double)~System.Double;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
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 0a4fa84ce1..dd8337abee 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
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -9,7 +10,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.UI;
-using osuTK;
namespace osu.Game.Rulesets.Pippidon.Beatmaps
{
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps
};
}
- private int getLane(HitObject hitObject) => (int)MathHelper.Clamp(
+ private int getLane(HitObject hitObject) => (int)Math.Clamp(
(getUsablePosition(hitObject) - minPosition) / (maxPosition - minPosition) * PippidonPlayfield.LANE_COUNT, 0, PippidonPlayfield.LANE_COUNT - 1);
private float getUsablePosition(HitObject h) => (h as IHasYPosition)?.Y ?? ((IHasXPosition)h).X;
diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs
index 8cdb490922..810dc7eed5 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.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.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -110,7 +111,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Argon
double duration = ObjectState.HitObject.StartTime - ObjectState.DisplayStartTime;
- fadeContent.Alpha = MathHelper.Clamp(
+ fadeContent.Alpha = Math.Clamp(
Interpolation.ValueAt(
Time.Current, 1f, 0f,
ObjectState.DisplayStartTime + duration * lens_flare_start,
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs
index 8ed9d0476a..7a5b01ce79 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -76,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
if (hasReachedObject && showHitMarkers.Value)
{
float alpha = Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION, Easing.In);
- float ringScale = MathHelper.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1);
+ float ringScale = Math.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1);
ring.Scale = new Vector2(1 + 0.1f * ringScale);
content.Alpha = 0.9f * (1 - alpha);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 39c0681dba..52575bdd67 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -270,14 +270,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (adjustVelocity)
{
proposedVelocity = proposedDistance / oldDuration;
- proposedDistance = MathHelper.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration);
+ proposedDistance = Math.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration);
}
else
{
double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1;
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance;
- proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
+ proposedDistance = Math.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
}
if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier))
diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs
index ddb2e02fb8..dd60e303f6 100644
--- a/osu.Game/Overlays/NotificationOverlayToastTray.cs
+++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs
@@ -174,7 +174,7 @@ namespace osu.Game.Overlays
}
height = toastFlow.DrawHeight + 120;
- alpha = MathHelper.Clamp(toastFlow.DrawHeight / 41, 0, 1) * maxNotificationAlpha;
+ alpha = Math.Clamp(toastFlow.DrawHeight / 41, 0, 1) * maxNotificationAlpha;
}
toastContentBackground.Height = (float)Interpolation.DampContinuously(toastContentBackground.Height, height, 10, Clock.ElapsedFrameTime);
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 8bfa8dd6ff..19190ac362 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -278,17 +278,17 @@ namespace osu.Game.Screens.Play
processDrawables(rulesetComponents);
if (lowestTopScreenSpaceRight.HasValue)
- TopRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - TopRightElements.DrawHeight);
+ TopRightElements.Y = Math.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - TopRightElements.DrawHeight);
else
TopRightElements.Y = 0;
if (lowestTopScreenSpaceLeft.HasValue)
- LeaderboardFlow.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight);
+ LeaderboardFlow.Y = Math.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight);
else
LeaderboardFlow.Y = 0;
if (highestBottomScreenSpace.HasValue)
- bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight);
+ bottomRightElements.Y = BottomScoringElementsHeight = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight);
else
bottomRightElements.Y = 0;
diff --git a/osu.Game/Screens/Utility/CircleGameplay.cs b/osu.Game/Screens/Utility/CircleGameplay.cs
index 1f970c5121..0f328d04fb 100644
--- a/osu.Game/Screens/Utility/CircleGameplay.cs
+++ b/osu.Game/Screens/Utility/CircleGameplay.cs
@@ -201,8 +201,8 @@ namespace osu.Game.Screens.Utility
{
double preempt = (float)IBeatmapDifficultyInfo.DifficultyRange(SampleApproachRate.Value, 1800, 1200, 450);
- approach.Scale = new Vector2(1 + 4 * (float)MathHelper.Clamp((HitTime - Clock.CurrentTime) / preempt, 0, 100));
- Alpha = (float)MathHelper.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1);
+ approach.Scale = new Vector2(1 + 4 * (float)Math.Clamp((HitTime - Clock.CurrentTime) / preempt, 0, 100));
+ Alpha = (float)Math.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1);
if (Clock.CurrentTime > HitTime + duration)
Expire();
diff --git a/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs b/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs
index dcfcf602bf..ef1b848945 100644
--- a/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs
+++ b/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.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.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
@@ -55,22 +56,22 @@ namespace osu.Game.Screens.Utility.SampleComponents
{
case Key.F:
case Key.Up:
- box.Y = MathHelper.Clamp(box.Y - movementAmount, 0.1f, 0.9f);
+ box.Y = Math.Clamp(box.Y - movementAmount, 0.1f, 0.9f);
break;
case Key.J:
case Key.Down:
- box.Y = MathHelper.Clamp(box.Y + movementAmount, 0.1f, 0.9f);
+ box.Y = Math.Clamp(box.Y + movementAmount, 0.1f, 0.9f);
break;
case Key.Z:
case Key.Left:
- box.X = MathHelper.Clamp(box.X - movementAmount, 0.1f, 0.9f);
+ box.X = Math.Clamp(box.X - movementAmount, 0.1f, 0.9f);
break;
case Key.X:
case Key.Right:
- box.X = MathHelper.Clamp(box.X + movementAmount, 0.1f, 0.9f);
+ box.X = Math.Clamp(box.X + movementAmount, 0.1f, 0.9f);
break;
}
}
diff --git a/osu.Game/Screens/Utility/ScrollingGameplay.cs b/osu.Game/Screens/Utility/ScrollingGameplay.cs
index 5038c53b4a..c0264f5734 100644
--- a/osu.Game/Screens/Utility/ScrollingGameplay.cs
+++ b/osu.Game/Screens/Utility/ScrollingGameplay.cs
@@ -165,7 +165,7 @@ namespace osu.Game.Screens.Utility
{
double preempt = (float)IBeatmapDifficultyInfo.DifficultyRange(SampleApproachRate.Value, 1800, 1200, 450);
- Alpha = (float)MathHelper.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1);
+ Alpha = (float)Math.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1);
Y = judgement_position - (float)((HitTime - Clock.CurrentTime) / preempt);
if (Clock.CurrentTime > HitTime + duration)
From 88089fb0144a54d99b2e586f2d1b8e4512494604 Mon Sep 17 00:00:00 2001
From: Zihad
Date: Fri, 28 Feb 2025 19:03:39 +0600
Subject: [PATCH 151/262] make `SettingsPanel.SearchTextBox`'s setter private
---
osu.Game/Overlays/SettingsPanel.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs
index d8b054eaf8..9b268c573f 100644
--- a/osu.Game/Overlays/SettingsPanel.cs
+++ b/osu.Game/Overlays/SettingsPanel.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Overlays
public SettingsSectionsContainer SectionsContainer { get; private set; }
- protected SeekLimitedSearchTextBox SearchTextBox;
+ protected SeekLimitedSearchTextBox SearchTextBox { get; private set; }
protected override string PopInSampleName => "UI/settings-pop-in";
protected override double PopInOutSampleBalance => -OsuGameBase.SFX_STEREO_STRENGTH;
From 0d7c00ae09d65d7c4a53abd1860d3029e1c004bd Mon Sep 17 00:00:00 2001
From: Zihad
Date: Fri, 28 Feb 2025 19:04:47 +0600
Subject: [PATCH 152/262] use `Bindable.SetDefault` for clearing search text
---
osu.Game/Overlays/SettingsOverlay.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs
index 8a39d75565..630675a717 100644
--- a/osu.Game/Overlays/SettingsOverlay.cs
+++ b/osu.Game/Overlays/SettingsOverlay.cs
@@ -69,7 +69,7 @@ namespace osu.Game.Overlays
where T : Drawable
{
// if search isn't cleared then the target control won't be visible if it doesn't match the query
- SearchTextBox.Current.Value = "";
+ SearchTextBox.Current.SetDefault();
Show();
From 8032b6893274a152a12226572e89a000262c5583 Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 28 Feb 2025 16:59:39 +0900
Subject: [PATCH 153/262] Stop using padding for panel x offsets
---
osu.Game/Screens/SelectV2/PanelBase.cs | 11 +++++++----
osu.Game/Screens/SelectV2/PanelBeatmap.cs | 4 ++--
2 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs
index 2a32b1a95f..1dc645ba53 100644
--- a/osu.Game/Screens/SelectV2/PanelBase.cs
+++ b/osu.Game/Screens/SelectV2/PanelBase.cs
@@ -61,6 +61,11 @@ namespace osu.Game.Screens.SelectV2
}
}
+ // content is offset by PanelXOffset, make sure we only handle input at the actual visible
+ // offset region.
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
+ TopLevelContent.ReceivePositionalInputAt(screenSpacePos);
+
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colours)
{
@@ -219,8 +224,6 @@ namespace osu.Game.Screens.SelectV2
private void updateDisplay()
{
- backgroundLayer.TransformTo(nameof(Padding), backgroundLayer.Padding with { Vertical = Expanded.Value ? 2f : 0f }, duration, Easing.OutQuint);
-
var backgroundColour = accentColour ?? Color4.White;
var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF");
@@ -235,7 +238,7 @@ namespace osu.Game.Screens.SelectV2
private void updateXOffset()
{
- float x = PanelXOffset;
+ float x = PanelXOffset + corner_radius;
if (!Expanded.Value && !Selected.Value)
x += active_x_offset;
@@ -243,7 +246,7 @@ namespace osu.Game.Screens.SelectV2
if (!KeyboardSelected.Value)
x += active_x_offset * 0.5f;
- this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint);
+ TopLevelContent.MoveToX(x, duration, Easing.OutQuint);
}
private void updateHover()
diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs
index 0ce6b1a9a2..d4bf3519fa 100644
--- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs
+++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Screens.SelectV2
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
- var inputRectangle = DrawRectangle;
+ var inputRectangle = TopLevelContent.DrawRectangle;
// Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel.
//
@@ -62,7 +62,7 @@ namespace osu.Game.Screens.SelectV2
// larger hit target.
inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING });
- return inputRectangle.Contains(ToLocalSpace(screenSpacePos));
+ return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos));
}
[BackgroundDependencyLoader]
From 29c35529d27b730847d03896c04c03a9e95efd3b Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 28 Feb 2025 17:02:09 +0900
Subject: [PATCH 154/262] Fix activation flash being applied twice (and adjust
duration)
---
osu.Game/Screens/SelectV2/PanelBase.cs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs
index 1dc645ba53..b9d9bbd20a 100644
--- a/osu.Game/Screens/SelectV2/PanelBase.cs
+++ b/osu.Game/Screens/SelectV2/PanelBase.cs
@@ -217,7 +217,6 @@ namespace osu.Game.Screens.SelectV2
protected override bool OnClick(ClickEvent e)
{
- activationFlash.FadeOutFromOne(500, Easing.OutQuint);
carousel?.Activate(Item!);
return true;
}
@@ -287,7 +286,7 @@ namespace osu.Game.Screens.SelectV2
public virtual void Activated()
{
- activationFlash.FadeOutFromOne(500, Easing.OutQuint);
+ activationFlash.FadeOutFromOne(1000, Easing.OutQuint);
}
#endregion
From 4beac64bdb6c2dee8492ea8b113498b78ef5f36a Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 28 Feb 2025 17:19:30 +0900
Subject: [PATCH 155/262] Remove unused container level
---
osu.Game/Screens/SelectV2/PanelBase.cs | 43 ++++++++++++--------------
1 file changed, 19 insertions(+), 24 deletions(-)
diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs
index b9d9bbd20a..36f4f13a3b 100644
--- a/osu.Game/Screens/SelectV2/PanelBase.cs
+++ b/osu.Game/Screens/SelectV2/PanelBase.cs
@@ -32,7 +32,6 @@ namespace osu.Game.Screens.SelectV2
private Box backgroundBorder = null!;
private Box backgroundGradient = null!;
private Box backgroundAccentGradient = null!;
- private Container backgroundLayer = null!;
private Container backgroundLayerHorizontalPadding = null!;
private Container backgroundContainer = null!;
private Container iconContainer = null!;
@@ -66,6 +65,9 @@ namespace osu.Game.Screens.SelectV2
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
TopLevelContent.ReceivePositionalInputAt(screenSpacePos);
+ [Resolved]
+ private BeatmapCarousel? carousel { get; set; }
+
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colours)
{
@@ -102,30 +104,26 @@ namespace osu.Game.Screens.SelectV2
backgroundLayerHorizontalPadding = new Container
{
RelativeSizeAxes = Axes.Both,
- Child = backgroundLayer = new Container
+ Child = new Container
{
RelativeSizeAxes = Axes.Both,
- Child = new Container
+ Masking = true,
+ CornerRadius = corner_radius,
+ Children = new Drawable[]
{
- Masking = true,
- CornerRadius = corner_radius,
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
+ backgroundGradient = new Box
{
- backgroundGradient = new Box
- {
- RelativeSizeAxes = Axes.Both,
- },
- backgroundAccentGradient = new Box
- {
- RelativeSizeAxes = Axes.Both,
- },
- backgroundContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- },
- }
- },
+ RelativeSizeAxes = Axes.Both,
+ },
+ backgroundAccentGradient = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ backgroundContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ }
},
}
},
@@ -212,9 +210,6 @@ namespace osu.Game.Screens.SelectV2
this.FadeInFromZero(duration, Easing.OutQuint);
}
- [Resolved]
- private BeatmapCarousel? carousel { get; set; }
-
protected override bool OnClick(ClickEvent e)
{
carousel?.Activate(Item!);
From 38de3566b14b4d08a17c806f2891fa85c82dfafd Mon Sep 17 00:00:00 2001
From: Dean Herbert
Date: Fri, 28 Feb 2025 17:37:18 +0900
Subject: [PATCH 156/262] Adjust set panel display and animations slightly
---
.../SelectV2/BeatmapSetPanelBackground.cs | 2 +-
osu.Game/Screens/SelectV2/PanelBase.cs | 12 ++++++------
osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 16 +++++++++++-----
3 files changed, 18 insertions(+), 12 deletions(-)
diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs
index 435a0ad262..798acf62ee 100644
--- a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs
+++ b/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapSetPanelBackground : ModelBackedDrawable
{
- protected override bool TransformImmediately => true;
+ protected override double TransformDuration => 400;
public WorkingBeatmap? Beatmap
{
diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs
index 36f4f13a3b..05a1a55c03 100644
--- a/osu.Game/Screens/SelectV2/PanelBase.cs
+++ b/osu.Game/Screens/SelectV2/PanelBase.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Screens.SelectV2
private const float active_x_offset = 50f;
- private const float duration = 400;
+ protected const float DURATION = 400;
protected float PanelXOffset { get; init; }
@@ -207,7 +207,7 @@ namespace osu.Game.Screens.SelectV2
protected override void PrepareForUse()
{
base.PrepareForUse();
- this.FadeInFromZero(duration, Easing.OutQuint);
+ this.FadeInFromZero(DURATION, Easing.OutQuint);
}
protected override bool OnClick(ClickEvent e)
@@ -221,10 +221,10 @@ namespace osu.Game.Screens.SelectV2
var backgroundColour = accentColour ?? Color4.White;
var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF");
- backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), duration, Easing.OutQuint);
- backgroundBorder.FadeColour(backgroundColour, duration, Easing.OutQuint);
+ backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), DURATION, Easing.OutQuint);
+ backgroundBorder.FadeColour(backgroundColour, DURATION, Easing.OutQuint);
- TopLevelContent.FadeEdgeEffectTo(Expanded.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), duration, Easing.OutQuint);
+ TopLevelContent.FadeEdgeEffectTo(Expanded.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), DURATION, Easing.OutQuint);
updateXOffset();
updateHover();
@@ -240,7 +240,7 @@ namespace osu.Game.Screens.SelectV2
if (!KeyboardSelected.Value)
x += active_x_offset * 0.5f;
- TopLevelContent.MoveToX(x, duration, Easing.OutQuint);
+ TopLevelContent.MoveToX(x, DURATION, Easing.OutQuint);
}
private void updateHover()
diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs
index 5c38fe8e04..512fbacec1 100644
--- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs
+++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Screens.SelectV2
Icon = chevronIcon = new Container
{
- Size = new Vector2(22),
+ Size = new Vector2(0, 22),
Child = new SpriteIcon
{
Anchor = Anchor.Centre,
@@ -128,10 +128,16 @@ namespace osu.Game.Screens.SelectV2
private void onExpanded()
{
- const float duration = 500;
-
- chevronIcon.ResizeWidthTo(Expanded.Value ? 22 : 0f, duration, Easing.OutQuint);
- chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint);
+ if (Expanded.Value)
+ {
+ chevronIcon.ResizeWidthTo(18, 600, Easing.OutElasticQuarter);
+ chevronIcon.FadeTo(1f, DURATION, Easing.OutQuint);
+ }
+ else
+ {
+ chevronIcon.ResizeWidthTo(0f, DURATION, Easing.OutQuint);
+ chevronIcon.FadeTo(0f, DURATION, Easing.OutQuint);
+ }
}
protected override void PrepareForUse()
From 881534eb7f3d71e817d511c64ca368e0e6eca069 Mon Sep 17 00:00:00 2001
From: Jamie Taylor
Date: Sat, 1 Mar 2025 01:51:37 +0900
Subject: [PATCH 157/262] Add SFX for kiai/star fountain activation
---
osu.Game/Screens/Menu/KiaiMenuFountains.cs | 14 +++++++++++++-
osu.Game/Screens/Play/KiaiGameplayFountains.cs | 14 +++++++++++++-
2 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/osu.Game/Screens/Menu/KiaiMenuFountains.cs b/osu.Game/Screens/Menu/KiaiMenuFountains.cs
index 7978e9fa91..dbbff4a9f5 100644
--- a/osu.Game/Screens/Menu/KiaiMenuFountains.cs
+++ b/osu.Game/Screens/Menu/KiaiMenuFountains.cs
@@ -3,6 +3,8 @@
using System;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
@@ -14,8 +16,11 @@ namespace osu.Game.Screens.Menu
private StarFountain leftFountain = null!;
private StarFountain rightFountain = null!;
+ private Sample? sample;
+ private SampleChannel? sampleChannel;
+
[BackgroundDependencyLoader]
- private void load()
+ private void load(AudioManager audio)
{
RelativeSizeAxes = Axes.Both;
@@ -34,6 +39,8 @@ namespace osu.Game.Screens.Menu
X = -250,
},
};
+
+ sample = audio.Samples.Get(@"Gameplay/fountain-shoot");
}
private bool isTriggered;
@@ -73,6 +80,11 @@ namespace osu.Game.Screens.Menu
rightFountain.Shoot(1);
break;
}
+
+ // Track sample channel to avoid overlapping playback
+ sampleChannel?.Stop();
+ sampleChannel = sample?.GetChannel();
+ sampleChannel?.Play();
}
}
}
diff --git a/osu.Game/Screens/Play/KiaiGameplayFountains.cs b/osu.Game/Screens/Play/KiaiGameplayFountains.cs
index d4e61dc5a0..7e09f50133 100644
--- a/osu.Game/Screens/Play/KiaiGameplayFountains.cs
+++ b/osu.Game/Screens/Play/KiaiGameplayFountains.cs
@@ -3,6 +3,8 @@
using System;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
@@ -19,8 +21,11 @@ namespace osu.Game.Screens.Play
private Bindable kiaiStarFountains = null!;
+ private Sample? sample;
+ private SampleChannel? sampleChannel;
+
[BackgroundDependencyLoader]
- private void load(OsuConfigManager config)
+ private void load(OsuConfigManager config, AudioManager audio)
{
kiaiStarFountains = config.GetBindable(OsuSetting.StarFountains);
@@ -41,6 +46,8 @@ namespace osu.Game.Screens.Play
X = -75,
},
};
+
+ sample = audio.Samples.Get(@"Gameplay/fountain-shoot");
}
private bool isTriggered;
@@ -66,6 +73,11 @@ namespace osu.Game.Screens.Play
{
leftFountain.Shoot(1);
rightFountain.Shoot(-1);
+
+ // Track sample channel to avoid overlapping playback
+ sampleChannel?.Stop();
+ sampleChannel = sample?.GetChannel();
+ sampleChannel?.Play();
}
public partial class GameplayStarFountain : StarFountain
From ec6ff240f38ef69d37c50437c8f97b5fa3804c90 Mon Sep 17 00:00:00 2001
From: "Giovanni D."
Date: Sun, 2 Mar 2025 00:49:04 -0800
Subject: [PATCH 158/262] Add taskbar flashing when a multiplayer game is
starting
---
osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index 111b453adb..e5bc683d19 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -30,6 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved]
private MultiplayerClient client { get; set; } = null!;
+ [Resolved]
+ private OsuGame? game { get; set; }
+
private IBindable isConnected = null!;
private readonly TaskCompletionSource