diff --git a/Directory.Build.props b/Directory.Build.props
index 53ad973e47..894ea25c8b 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -16,7 +16,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index e28053d0ca..e9b92be0c3 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 027bd0b7e2..e145dd7b69 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index e2c715d385..a301432a6c 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 027bd0b7e2..e145dd7b69 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 0bcbfc4baf..fec96c9165 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -20,6 +20,7 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
+ [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-beatmap-archive")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-skin-archive")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-replay")]
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 6457ec92da..4c8b9b2b08 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
index f1b51e51d0..6f3e6763bd 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
- ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
+ ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
};
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
index d53d019e90..1b7d254321 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
- ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
+ ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
};
}
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index 674a22df98..fad39ef9d6 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
index 6ae854e7f3..86f667466f 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
- ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
+ ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
};
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs
index 064c55ed8d..1c06bb389b 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score
{
- ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
+ ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
};
}
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index 15675e74d1..7cd06c5225 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.5867229481955389d, "diffcalc-test")]
- [TestCase(1.0416315570967911d, "zero-length-sliders")]
+ [TestCase(6.5295339534769958d, "diffcalc-test")]
+ [TestCase(1.1514260533755143d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);
- [TestCase(8.2730989071947896d, "diffcalc-test")]
- [TestCase(1.2726413186221039d, "zero-length-sliders")]
+ [TestCase(9.047752485219954d, "diffcalc-test")]
+ [TestCase(1.3985711787077566d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime());
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.cs
new file mode 100644
index 0000000000..ef05bcd320
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoSpinnerStacking.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Osu.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ [TestFixture]
+ public class TestSceneNoSpinnerStacking : TestSceneOsuPlayer
+ {
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
+ {
+ var beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BaseDifficulty = new BeatmapDifficulty { OverallDifficulty = 10 },
+ Ruleset = ruleset
+ }
+ };
+
+ for (int i = 0; i < 512; i++)
+ {
+ if (i % 32 < 20)
+ beatmap.HitObjects.Add(new Spinner { Position = new Vector2(256, 192), StartTime = i * 200, EndTime = (i * 200) + 100 });
+ }
+
+ return beatmap;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index f5f1159542..66f4ad3d3f 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 49ac6a7af3..4b90285fd4 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -12,20 +12,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
public class OsuDifficultyHitObject : DifficultyHitObject
{
- private const int normalized_radius = 52;
+ private const int normalized_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
+ private const int min_delta_time = 25;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
- ///
- /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms to account for simultaneous s.
- ///
- public double StrainTime { get; private set; }
-
///
/// Normalized distance from the end position of the previous to the start position of this .
///
public double JumpDistance { get; private set; }
+ ///
+ /// Minimum distance from the end position of the previous to the start position of this .
+ ///
+ public double MovementDistance { get; private set; }
+
///
/// Normalized distance between the start and end position of the previous .
///
@@ -37,6 +38,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
///
public double? Angle { get; private set; }
+ ///
+ /// Milliseconds elapsed since the end time of the previous , with a minimum of 25ms.
+ ///
+ public double MovementTime { get; private set; }
+
+ ///
+ /// Milliseconds elapsed since the start time of the previous to the end time of the same previous , with a minimum of 25ms.
+ ///
+ public double TravelTime { get; private set; }
+
+ ///
+ /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms.
+ ///
+ public readonly double StrainTime;
+
private readonly OsuHitObject lastLastObject;
private readonly OsuHitObject lastObject;
@@ -46,13 +62,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
this.lastLastObject = (OsuHitObject)lastLastObject;
this.lastObject = (OsuHitObject)lastObject;
- setDistances();
+ // Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
+ StrainTime = Math.Max(DeltaTime, min_delta_time);
- // Capped to 25ms to prevent difficulty calculation breaking from simulatenous objects.
- StrainTime = Math.Max(DeltaTime, 25);
+ setDistances(clockRate);
}
- private void setDistances()
+ private void setDistances(double clockRate)
{
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
if (BaseObject is Spinner || lastObject is Spinner)
@@ -67,15 +83,29 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
scalingFactor *= 1 + smallCircleBonus;
}
+ Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
+ JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
+
if (lastObject is Slider lastSlider)
{
computeSliderCursorPosition(lastSlider);
TravelDistance = lastSlider.LazyTravelDistance * scalingFactor;
+ TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
+ MovementTime = Math.Max(StrainTime - TravelTime, min_delta_time);
+
+ // Jump distance from the slider tail to the next object, as opposed to the lazy position of JumpDistance.
+ float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor;
+
+ // For hitobjects which continue in the direction of the slider, the player will normally follow through the slider,
+ // such that they're not jumping from the lazy position but rather from very close to (or the end of) the slider.
+ // In such cases, a leniency is applied by also considering the jump distance from the tail of the slider, and taking the minimum jump distance.
+ MovementDistance = Math.Min(JumpDistance, tailJumpDistance);
+ }
+ else
+ {
+ MovementTime = StrainTime;
+ MovementDistance = JumpDistance;
}
-
- Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
-
- JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
if (lastLastObject != null && !(lastLastObject is Spinner))
{
@@ -98,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyEndPosition = slider.StackedPosition;
- float approxFollowCircleRadius = (float)(slider.Radius * 3);
+ float followCircleRadius = (float)(slider.Radius * 2.4);
var computeVertex = new Action(t =>
{
double progress = (t - slider.StartTime) / slider.SpanDuration;
@@ -111,11 +141,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
var diff = slider.StackedPosition + slider.Path.PositionAt(progress) - slider.LazyEndPosition.Value;
float dist = diff.Length;
- if (dist > approxFollowCircleRadius)
+ slider.LazyTravelTime = t - slider.StartTime;
+
+ if (dist > followCircleRadius)
{
// The cursor would be outside the follow circle, we need to move it
diff.Normalize(); // Obtain direction of diff
- dist -= approxFollowCircleRadius;
+ dist -= followCircleRadius;
slider.LazyEndPosition += diff * dist;
slider.LazyTravelDistance += dist;
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index 64ca567a15..a054b46366 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -14,53 +14,96 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
public class Aim : OsuStrainSkill
{
- private const double angle_bonus_begin = Math.PI / 3;
- private const double timing_threshold = 107;
-
public Aim(Mod[] mods)
: base(mods)
{
}
+ protected override int HistoryLength => 2;
+
+ private const double wide_angle_multiplier = 1.5;
+ private const double acute_angle_multiplier = 2.0;
+
private double currentStrain = 1;
- private double skillMultiplier => 26.25;
+ private double skillMultiplier => 23.25;
private double strainDecayBase => 0.15;
private double strainValueOf(DifficultyHitObject current)
{
- if (current.BaseObject is Spinner)
+ if (current.BaseObject is Spinner || Previous.Count <= 1 || Previous[0].BaseObject is Spinner)
return 0;
- var osuCurrent = (OsuDifficultyHitObject)current;
+ var osuCurrObj = (OsuDifficultyHitObject)current;
+ var osuLastObj = (OsuDifficultyHitObject)Previous[0];
+ var osuLastLastObj = (OsuDifficultyHitObject)Previous[1];
- double aimStrain = 0;
+ // Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
+ double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime;
- if (Previous.Count > 0)
+ // But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
+ if (osuLastObj.BaseObject is Slider)
{
- var osuPrevious = (OsuDifficultyHitObject)Previous[0];
+ double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to current object
+ double travelVelocity = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // calculate the slider velocity from slider head to slider end.
- if (osuCurrent.Angle != null && osuCurrent.Angle.Value > angle_bonus_begin)
+ currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity.
+ }
+
+ // As above, do the same for the previous hitobject.
+ double prevVelocity = osuLastObj.JumpDistance / osuLastObj.StrainTime;
+
+ if (osuLastLastObj.BaseObject is Slider)
+ {
+ double movementVelocity = osuLastObj.MovementDistance / osuLastObj.MovementTime;
+ double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime;
+
+ prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity);
+ }
+
+ double angleBonus = 0;
+ double aimStrain = currVelocity; // Start strain with regular velocity.
+
+ 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)
{
- const double scale = 90;
+ double currAngle = osuCurrObj.Angle.Value;
+ double lastAngle = osuLastObj.Angle.Value;
+ double lastLastAngle = osuLastLastObj.Angle.Value;
- double angleBonus = Math.Sqrt(
- Math.Max(osuPrevious.JumpDistance - scale, 0)
- * Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2)
- * Math.Max(osuCurrent.JumpDistance - scale, 0));
- aimStrain = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
+ // Rewarding angles, take the smaller velocity as base.
+ angleBonus = Math.Min(currVelocity, prevVelocity);
+
+ double wideAngleBonus = calcWideAngleBonus(currAngle);
+ double acuteAngleBonus = calcAcuteAngleBonus(currAngle);
+
+ if (osuCurrObj.StrainTime > 100) // 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, 125 / 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.JumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
+ }
+
+ wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3))); // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
+ acuteAngleBonus *= 0.5 + 0.5 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3))); // Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse.
+
+ angleBonus = acuteAngleBonus * acute_angle_multiplier + wideAngleBonus * wide_angle_multiplier; // add the angle buffs together.
}
}
- double jumpDistanceExp = applyDiminishingExp(osuCurrent.JumpDistance);
- double travelDistanceExp = applyDiminishingExp(osuCurrent.TravelDistance);
+ aimStrain += angleBonus; // Add in angle bonus.
- return Math.Max(
- aimStrain + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold),
- (Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / osuCurrent.StrainTime
- );
+ return aimStrain;
}
+ private 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 double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle);
+
private double applyDiminishingExp(double val) => Math.Pow(val, 0.99);
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
index 7c45b2bc07..8b7de9e109 100644
--- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Objects
set => StackHeightBindable.Value = value;
}
- public Vector2 StackOffset => new Vector2(StackHeight * Scale * -6.4f);
+ public virtual Vector2 StackOffset => new Vector2(StackHeight * Scale * -6.4f);
public double Radius => OBJECT_RADIUS * Scale;
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 9b2babb9ff..5c1c3fd253 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -79,6 +79,12 @@ namespace osu.Game.Rulesets.Osu.Objects
///
internal float LazyTravelDistance;
+ ///
+ /// The time taken by the cursor upon completion of this if it was hit
+ /// with as few movements as possible. This is set and used by difficulty calculation.
+ ///
+ internal double LazyTravelTime;
+
public IList> NodeSamples { get; set; } = new List>();
[JsonIgnore]
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index f85dc0d391..0ad8e4ea68 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
@@ -8,6 +8,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects
{
@@ -31,6 +32,8 @@ namespace osu.Game.Rulesets.Osu.Objects
///
public int MaximumBonusSpins { get; protected set; } = 1;
+ public override Vector2 StackOffset => Vector2.Zero;
+
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index b9b295767e..568e35c221 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
index c75714032e..ecc9c92025 100644
--- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
+++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
@@ -38,6 +38,15 @@ namespace osu.Game.Tests.Skins.IO
assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu);
});
+ [Test]
+ public Task TestSingleImportMissingSectionHeader() => runSkinTest(async osu =>
+ {
+ var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner", includeSectionHeader: false), "skin.osk"));
+
+ // When the import filename doesn't match, it should be appended (and update the skin.ini).
+ assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu);
+ });
+
[Test]
public Task TestSingleImportMatchingFilename() => runSkinTest(async osu =>
{
@@ -199,21 +208,23 @@ namespace osu.Game.Tests.Skins.IO
return zipStream;
}
- private MemoryStream createOskWithIni(string name, string author, bool makeUnique = false, string iniFilename = @"skin.ini")
+ private MemoryStream createOskWithIni(string name, string author, bool makeUnique = false, string iniFilename = @"skin.ini", bool includeSectionHeader = true)
{
var zipStream = new MemoryStream();
using var zip = ZipArchive.Create();
- zip.AddEntry(iniFilename, generateSkinIni(name, author, makeUnique));
+ zip.AddEntry(iniFilename, generateSkinIni(name, author, makeUnique, includeSectionHeader));
zip.SaveTo(zipStream);
return zipStream;
}
- private MemoryStream generateSkinIni(string name, string author, bool makeUnique = true)
+ private MemoryStream generateSkinIni(string name, string author, bool makeUnique = true, bool includeSectionHeader = true)
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
- writer.WriteLine("[General]");
+ if (includeSectionHeader)
+ writer.WriteLine("[General]");
+
writer.WriteLine($"Name: {name}");
writer.WriteLine($"Author: {author}");
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 11caf9f498..38cf9d662f 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -231,6 +231,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
}
});
+
+ AddAssert("Check participant count correct", () => client.APIRoom?.ParticipantCount.Value == 1);
+ AddAssert("Check participant list contains user", () => client.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1);
}
[Test]
@@ -290,6 +293,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => client.Room != null);
+
+ AddAssert("Check participant count correct", () => client.APIRoom?.ParticipantCount.Value == 1);
+ AddAssert("Check participant list contains user", () => client.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
index e50b150f94..2549681519 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
@@ -45,11 +45,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
- public void TestAddNullUser()
+ public void TestAddUnresolvedUser()
{
AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1);
- AddStep("add non-resolvable user", () => Client.AddNullUser());
+ AddStep("add non-resolvable user", () => Client.TestAddUnresolvedUser());
AddAssert("null user added", () => Client.Room.AsNonNull().Users.Count(u => u.User == null) == 1);
AddUntilStep("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2);
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index cd56cb51ae..57815d9273 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -3,7 +3,7 @@
-
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 2673c9ec9f..c0f94d49c7 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
index 9a0cdb387d..035f438b89 100644
--- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
@@ -131,7 +131,7 @@ namespace osu.Game.Beatmaps
var localRulesetInfo = rulesetInfo as RulesetInfo;
// Difficulty can only be computed if the beatmap and ruleset are locally available.
- if (localBeatmapInfo == null || localRulesetInfo == null)
+ if (localBeatmapInfo == null || localBeatmapInfo.ID == 0 || localRulesetInfo == null)
{
// If not, fall back to the existing star difficulty (e.g. from an online source).
return Task.FromResult(new StarDifficulty(beatmapInfo.StarRating, (beatmapInfo as IBeatmapOnlineInfo)?.MaxCombo ?? 0));
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 0509a9db47..0caee8f9cd 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -249,6 +249,23 @@ namespace osu.Game.Beatmaps
public IBindable>> DownloadFailed => beatmapModelDownloader.DownloadFailed;
+ // Temporary method until this class supports IBeatmapSetInfo or otherwise.
+ public bool Download(IBeatmapSetInfo model, bool minimiseDownloadSize = false)
+ {
+ return beatmapModelDownloader.Download(new BeatmapSetInfo
+ {
+ OnlineBeatmapSetID = model.OnlineID,
+ Metadata = new BeatmapMetadata
+ {
+ Title = model.Metadata?.Title,
+ Artist = model.Metadata?.Artist,
+ TitleUnicode = model.Metadata?.TitleUnicode,
+ ArtistUnicode = model.Metadata?.ArtistUnicode,
+ Author = new User { Username = model.Metadata?.Author },
+ }
+ }, minimiseDownloadSize);
+ }
+
public bool Download(BeatmapSetInfo model, bool minimiseDownloadSize = false)
{
return beatmapModelDownloader.Download(model, minimiseDownloadSize);
diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
index 64412675bb..6e573cc2a0 100644
--- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
+++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
@@ -60,8 +60,9 @@ namespace osu.Game.Beatmaps.Drawables
/// The ruleset to show the difficulty with.
/// The mods to show the difficulty with.
/// Whether to display a tooltip when hovered.
- public DifficultyIcon([NotNull] IBeatmapInfo beatmapInfo, [CanBeNull] IRulesetInfo ruleset, [CanBeNull] IReadOnlyList mods, bool shouldShowTooltip = true)
- : this(beatmapInfo, shouldShowTooltip)
+ /// Whether to perform difficulty lookup (including calculation if necessary).
+ public DifficultyIcon([NotNull] IBeatmapInfo beatmapInfo, [CanBeNull] IRulesetInfo ruleset, [CanBeNull] IReadOnlyList mods, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true)
+ : this(beatmapInfo, shouldShowTooltip, performBackgroundDifficultyLookup)
{
this.ruleset = ruleset ?? beatmapInfo.Ruleset;
this.mods = mods ?? Array.Empty();
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 56525ddb14..0276abc3ff 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Beatmaps.Formats
protected override void ParseStreamInto(LineBufferedReader stream, T output)
{
- Section section = Section.None;
+ Section section = Section.General;
string line;
@@ -47,10 +47,7 @@ namespace osu.Game.Beatmaps.Formats
if (line.StartsWith('[') && line.EndsWith(']'))
{
if (!Enum.TryParse(line[1..^1], out section))
- {
Logger.Log($"Unknown section \"{line}\" in \"{output}\"");
- section = Section.None;
- }
OnBeginNewSection(section);
continue;
@@ -148,7 +145,6 @@ namespace osu.Game.Beatmaps.Formats
protected enum Section
{
- None,
General,
Editor,
Metadata,
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index d60c9cfe65..69d72226ba 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -107,7 +107,8 @@ namespace osu.Game.Online.API
WebRequest = CreateWebRequest();
WebRequest.Failed += Fail;
WebRequest.AllowRetryOnTimeout = false;
- WebRequest.AddHeader("Authorization", $"Bearer {API.AccessToken}");
+ if (!string.IsNullOrEmpty(API.AccessToken))
+ WebRequest.AddHeader("Authorization", $"Bearer {API.AccessToken}");
if (isFailing) return;
diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs
index 2e41723f34..5395fe0429 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs
@@ -37,6 +37,7 @@ namespace osu.Game.Online.API.Requests.Responses
public DateTimeOffset Date { get; set; }
[JsonProperty(@"beatmap")]
+ [CanBeNull]
public APIBeatmap Beatmap { get; set; }
[JsonProperty("accuracy")]
@@ -46,6 +47,7 @@ namespace osu.Game.Online.API.Requests.Responses
public double? PP { get; set; }
[JsonProperty(@"beatmapset")]
+ [CanBeNull]
public APIBeatmapSet BeatmapSet
{
set
@@ -96,7 +98,7 @@ namespace osu.Game.Online.API.Requests.Responses
{
TotalScore = TotalScore,
MaxCombo = MaxCombo,
- BeatmapInfo = Beatmap.ToBeatmapInfo(rulesets),
+ BeatmapInfo = Beatmap?.ToBeatmapInfo(rulesets),
User = User,
Accuracy = Accuracy,
OnlineScoreID = OnlineID,
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 28505f6b0e..0586e0ae60 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -148,6 +148,10 @@ namespace osu.Game.Online.Multiplayer
{
Room = joinedRoom;
APIRoom = room;
+
+ Debug.Assert(LocalUser != null);
+ addUserToAPIRoom(LocalUser);
+
foreach (var user in joinedRoom.Users)
updateUserPlayingState(user.UserID, user.State);
@@ -372,6 +376,8 @@ namespace osu.Game.Online.Multiplayer
Room.Users.Add(user);
+ addUserToAPIRoom(user);
+
UserJoined?.Invoke(user);
RoomUpdated?.Invoke();
});
@@ -391,6 +397,18 @@ namespace osu.Game.Online.Multiplayer
return handleUserLeft(user, UserKicked);
}
+ private void addUserToAPIRoom(MultiplayerRoomUser user)
+ {
+ Debug.Assert(APIRoom != null);
+
+ APIRoom.RecentParticipants.Add(user.User ?? new User
+ {
+ Id = user.UserID,
+ Username = "[Unresolved]"
+ });
+ APIRoom.ParticipantCount.Value++;
+ }
+
private Task handleUserLeft(MultiplayerRoomUser user, Action? callback)
{
if (Room == null)
@@ -404,6 +422,10 @@ namespace osu.Game.Online.Multiplayer
Room.Users.Remove(user);
PlayingUserIds.Remove(user.UserID);
+ Debug.Assert(APIRoom != null);
+ APIRoom.RecentParticipants.RemoveAll(u => u.Id == user.UserID);
+ APIRoom.ParticipantCount.Value--;
+
callback?.Invoke(user);
RoomUpdated?.Invoke();
}, false);
diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
index 6cd735af23..a642e283f9 100644
--- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
@@ -53,7 +53,10 @@ namespace osu.Game.Online.Rooms
downloadTracker?.RemoveAndDisposeImmediately();
downloadTracker = new BeatmapDownloadTracker(item.NewValue.Beatmap.Value.BeatmapSet);
- downloadTracker.State.BindValueChanged(_ => updateAvailability());
+
+ AddInternal(downloadTracker);
+
+ downloadTracker.State.BindValueChanged(_ => updateAvailability(), true);
downloadTracker.Progress.BindValueChanged(_ =>
{
if (downloadTracker.State.Value != DownloadState.Downloading)
@@ -63,9 +66,7 @@ namespace osu.Game.Online.Rooms
// we don't want to flood the network with this, so rate limit how often we send progress updates.
if (progressUpdate?.Completed != false)
progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500);
- });
-
- AddInternal(downloadTracker);
+ }, true);
}, true);
}
diff --git a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs
index 9e432fa99e..d5da6c401c 100644
--- a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs
+++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs
@@ -5,6 +5,7 @@ using System.Net.Http;
using Newtonsoft.Json;
using osu.Framework.IO.Network;
using osu.Game.Online.API;
+using osu.Game.Online.Solo;
using osu.Game.Scoring;
namespace osu.Game.Online.Rooms
@@ -14,14 +15,14 @@ namespace osu.Game.Online.Rooms
private readonly long scoreId;
private readonly long roomId;
private readonly long playlistItemId;
- private readonly ScoreInfo scoreInfo;
+ private readonly SubmittableScore score;
public SubmitRoomScoreRequest(long scoreId, long roomId, long playlistItemId, ScoreInfo scoreInfo)
{
this.scoreId = scoreId;
this.roomId = roomId;
this.playlistItemId = playlistItemId;
- this.scoreInfo = scoreInfo;
+ score = new SubmittableScore(scoreInfo);
}
protected override WebRequest CreateWebRequest()
@@ -31,7 +32,7 @@ namespace osu.Game.Online.Rooms
req.ContentType = "application/json";
req.Method = HttpMethod.Put;
- req.AddRaw(JsonConvert.SerializeObject(scoreInfo, new JsonSerializerSettings
+ req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
}));
diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs
index d7c2837f4d..5ed49cf384 100644
--- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs
@@ -86,7 +86,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
break;
default:
- beatmaps.Download(new BeatmapSetInfo { OnlineBeatmapSetID = beatmapSet.OnlineID }, noVideoSetting.Value);
+ beatmaps.Download(beatmapSet, noVideoSetting.Value);
break;
}
};
diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs
index 6862864c55..bd7723d3c0 100644
--- a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs
+++ b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs
@@ -108,7 +108,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
return;
}
- beatmaps.Download(new BeatmapSetInfo { OnlineBeatmapSetID = beatmapSet.OnlineID }, noVideo);
+ beatmaps.Download(beatmapSet, noVideo);
};
localUser.BindTo(api.LocalUser);
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
index ca5534dbc2..fb464e1b41 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
@@ -91,7 +91,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
{
new OsuSpriteText
{
- Text = $"{Score.Beatmap.DifficultyName}",
+ Text = $"{Score.Beatmap?.DifficultyName}",
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
Colour = colours.Yellow
},
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
index eab81186d5..5b4284dc2f 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
@@ -58,6 +58,11 @@ namespace osu.Game.Rulesets.Difficulty
return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
}
+ ///
+ /// Calculates the difficulty of the beatmap and returns a set of representing the difficulty at every relevant time value in the beatmap.
+ ///
+ /// The mods that should be applied to the beatmap.
+ /// The set of .
public List CalculateTimed(params Mod[] mods)
{
preProcess(mods);
@@ -77,7 +82,7 @@ namespace osu.Game.Rulesets.Difficulty
foreach (var skill in skills)
skill.ProcessInternal(hitObject);
- attribs.Add(new TimedDifficultyAttributes(hitObject.EndTime, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)));
+ attribs.Add(new TimedDifficultyAttributes(hitObject.EndTime * clockRate, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)));
}
return attribs;
diff --git a/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs
index 973b2dacb2..2509971389 100644
--- a/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs
+++ b/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs
@@ -11,9 +11,21 @@ namespace osu.Game.Rulesets.Difficulty
///
public class TimedDifficultyAttributes : IComparable
{
+ ///
+ /// The non-clock-adjusted time value at which the attributes take effect.
+ ///
public readonly double Time;
+
+ ///
+ /// The attributes.
+ ///
public readonly DifficultyAttributes Attributes;
+ ///
+ /// Creates new .
+ ///
+ /// The non-clock-adjusted time value at which the attributes take effect.
+ /// The attributes.
public TimedDifficultyAttributes(double time, DifficultyAttributes attributes)
{
Time = time;
diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs
index 69ab7225ac..6f947bd398 100644
--- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs
+++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs
@@ -105,7 +105,7 @@ namespace osu.Game.Screens.OnlinePlay
private void refresh()
{
- difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value, requiredMods) { Size = new Vector2(32) };
+ difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(32) };
beatmapText.Clear();
beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString(), null, text =>
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 76d36ae7d9..0739026544 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -194,68 +194,61 @@ namespace osu.Game.Skinning
string nameLine = @$"Name: {item.Name}";
string authorLine = @$"Author: {item.Creator}";
+ string[] newLines =
+ {
+ @"// The following content was automatically added by osu! during import, based on filename / folder metadata.",
+ @"[General]",
+ nameLine,
+ authorLine,
+ };
+
var existingFile = item.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase));
- if (existingFile != null)
+ if (existingFile == null)
{
- List outputLines = new List();
+ // In the case a skin doesn't have a skin.ini yet, let's create one.
+ writeNewSkinIni();
+ return;
+ }
- bool addedName = false;
- bool addedAuthor = false;
-
- using (var stream = Files.Storage.GetStream(existingFile.FileInfo.StoragePath))
- using (var sr = new StreamReader(stream))
+ using (Stream stream = new MemoryStream())
+ {
+ using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
- string line;
-
- while ((line = sr.ReadLine()) != null)
+ using (var existingStream = Files.Storage.GetStream(existingFile.FileInfo.StoragePath))
+ using (var sr = new StreamReader(existingStream))
{
- if (line.StartsWith(@"Name:", StringComparison.Ordinal))
- {
- outputLines.Add(nameLine);
- addedName = true;
- }
- else if (line.StartsWith(@"Author:", StringComparison.Ordinal))
- {
- outputLines.Add(authorLine);
- addedAuthor = true;
- }
- else
- outputLines.Add(line);
- }
- }
-
- if (!addedName || !addedAuthor)
- {
- outputLines.AddRange(new[]
- {
- @"[General]",
- nameLine,
- authorLine,
- });
- }
-
- using (Stream stream = new MemoryStream())
- {
- using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
- {
- foreach (string line in outputLines)
+ string line;
+ while ((line = sr.ReadLine()) != null)
sw.WriteLine(line);
}
- ReplaceFile(item, existingFile, stream);
+ sw.WriteLine();
+
+ foreach (string line in newLines)
+ sw.WriteLine(line);
+ }
+
+ ReplaceFile(item, existingFile, stream);
+
+ // can be removed 20220502.
+ if (!ensureIniWasUpdated(item))
+ {
+ Logger.Log($"Skin {item}'s skin.ini had issues and has been removed. Please report this and provide the problematic skin.", LoggingTarget.Database, LogLevel.Important);
+
+ DeleteFile(item, item.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase)));
+ writeNewSkinIni();
}
}
- else
+
+ void writeNewSkinIni()
{
using (Stream stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
- sw.WriteLine(@"[General]");
- sw.WriteLine(nameLine);
- sw.WriteLine(authorLine);
- sw.WriteLine(@"Version: latest");
+ foreach (string line in newLines)
+ sw.WriteLine(line);
}
AddFile(item, stream, @"skin.ini");
@@ -263,6 +256,17 @@ namespace osu.Game.Skinning
}
}
+ private bool ensureIniWasUpdated(SkinInfo item)
+ {
+ // This is a final consistency check to ensure that hash computation doesn't enter an infinite loop.
+ // With other changes to the surrounding code this should never be hit, but until we are 101% sure that there
+ // are no other cases let's avoid a hard startup crash by bailing and alerting.
+
+ var instance = GetSkin(item);
+
+ return instance.Configuration.SkinInfo.Name == item.Name;
+ }
+
protected override Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
{
var instance = GetSkin(model);
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index cd0f070d73..c6634abe82 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
return roomUser;
}
- public void AddNullUser() => addUser(new MultiplayerRoomUser(TestUserLookupCache.NULL_USER_ID));
+ public void TestAddUnresolvedUser() => addUser(new MultiplayerRoomUser(TestUserLookupCache.UNRESOLVED_USER_ID));
private void addUser(MultiplayerRoomUser user)
{
diff --git a/osu.Game/Tests/Visual/TestUserLookupCache.cs b/osu.Game/Tests/Visual/TestUserLookupCache.cs
index b73e81d0dd..fcb9c070ff 100644
--- a/osu.Game/Tests/Visual/TestUserLookupCache.cs
+++ b/osu.Game/Tests/Visual/TestUserLookupCache.cs
@@ -14,11 +14,11 @@ namespace osu.Game.Tests.Visual
/// A special user ID which would return a for.
/// As a simulation to what a regular would return in the case of failing to fetch the user.
///
- public const int NULL_USER_ID = -1;
+ public const int UNRESOLVED_USER_ID = -1;
protected override Task ComputeValueAsync(int lookup, CancellationToken token = default)
{
- if (lookup == NULL_USER_ID)
+ if (lookup == UNRESOLVED_USER_ID)
return Task.FromResult((User)null);
return Task.FromResult(new User
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 8052ab5254..c1c3336b5c 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -23,9 +23,9 @@
-
-
-
+
+
+
@@ -38,8 +38,8 @@
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index d152cb7066..0baf067a63 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -94,7 +94,7 @@
-
+