Merge branch 'master' into beatmap-card-basics

This commit is contained in:
Bartłomiej Dach 2021-11-03 00:11:03 +01:00
commit cf7545e36c
No known key found for this signature in database
GPG Key ID: BCECCD4FA41F6497
46 changed files with 340 additions and 142 deletions

View File

@ -16,7 +16,7 @@
<EmbeddedResource Include="Resources\**\*.*" /> <EmbeddedResource Include="Resources\**\*.*" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Code Analysis"> <ItemGroup Label="Code Analysis">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.2" PrivateAssets="All" /> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" /> <AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3" PrivateAssets="All" /> <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3" PrivateAssets="All" />
</ItemGroup> </ItemGroup>

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -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)] [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 = ".*\\\\.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 = ".*\\\\.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-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-skin-archive")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-replay")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-replay")]

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> 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(), Replay = new CatchAutoGenerator(beatmap).Generate(),
}; };
} }

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> 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(), Replay = new CatchAutoGenerator(beatmap).Generate(),
}; };
} }

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> 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(), Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
}; };
} }

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> 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(), Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
}; };
} }

View File

@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.5867229481955389d, "diffcalc-test")] [TestCase(6.5295339534769958d, "diffcalc-test")]
[TestCase(1.0416315570967911d, "zero-length-sliders")] [TestCase(1.1514260533755143d, "zero-length-sliders")]
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);
[TestCase(8.2730989071947896d, "diffcalc-test")] [TestCase(9.047752485219954d, "diffcalc-test")]
[TestCase(1.2726413186221039d, "zero-length-sliders")] [TestCase(1.3985711787077566d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime()); => Test(expected, name, new OsuModDoubleTime());

View File

@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.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;
}
}
}

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />

View File

@ -12,20 +12,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{ {
public class OsuDifficultyHitObject : DifficultyHitObject 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; protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms to account for simultaneous <see cref="OsuDifficultyHitObject"/>s.
/// </summary>
public double StrainTime { get; private set; }
/// <summary> /// <summary>
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>. /// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// </summary> /// </summary>
public double JumpDistance { get; private set; } public double JumpDistance { get; private set; }
/// <summary>
/// Minimum distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double MovementDistance { get; private set; }
/// <summary> /// <summary>
/// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>. /// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>.
/// </summary> /// </summary>
@ -37,6 +38,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
/// </summary> /// </summary>
public double? Angle { get; private set; } public double? Angle { get; private set; }
/// <summary>
/// Milliseconds elapsed since the end time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double MovementTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/> to the end time of the same previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double TravelTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public readonly double StrainTime;
private readonly OsuHitObject lastLastObject; private readonly OsuHitObject lastLastObject;
private readonly OsuHitObject lastObject; private readonly OsuHitObject lastObject;
@ -46,13 +62,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
this.lastLastObject = (OsuHitObject)lastLastObject; this.lastLastObject = (OsuHitObject)lastLastObject;
this.lastObject = (OsuHitObject)lastObject; 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. setDistances(clockRate);
StrainTime = Math.Max(DeltaTime, 25);
} }
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 // 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) if (BaseObject is Spinner || lastObject is Spinner)
@ -67,15 +83,29 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
scalingFactor *= 1 + smallCircleBonus; scalingFactor *= 1 + smallCircleBonus;
} }
Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
if (lastObject is Slider lastSlider) if (lastObject is Slider lastSlider)
{ {
computeSliderCursorPosition(lastSlider); computeSliderCursorPosition(lastSlider);
TravelDistance = lastSlider.LazyTravelDistance * scalingFactor; 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)) if (lastLastObject != null && !(lastLastObject is Spinner))
{ {
@ -98,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyEndPosition = slider.StackedPosition; slider.LazyEndPosition = slider.StackedPosition;
float approxFollowCircleRadius = (float)(slider.Radius * 3); float followCircleRadius = (float)(slider.Radius * 2.4);
var computeVertex = new Action<double>(t => var computeVertex = new Action<double>(t =>
{ {
double progress = (t - slider.StartTime) / slider.SpanDuration; 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; var diff = slider.StackedPosition + slider.Path.PositionAt(progress) - slider.LazyEndPosition.Value;
float dist = diff.Length; 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 // The cursor would be outside the follow circle, we need to move it
diff.Normalize(); // Obtain direction of diff diff.Normalize(); // Obtain direction of diff
dist -= approxFollowCircleRadius; dist -= followCircleRadius;
slider.LazyEndPosition += diff * dist; slider.LazyEndPosition += diff * dist;
slider.LazyTravelDistance += dist; slider.LazyTravelDistance += dist;
} }

View File

@ -14,53 +14,96 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// </summary> /// </summary>
public class Aim : OsuStrainSkill public class Aim : OsuStrainSkill
{ {
private const double angle_bonus_begin = Math.PI / 3;
private const double timing_threshold = 107;
public Aim(Mod[] mods) public Aim(Mod[] mods)
: base(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 currentStrain = 1;
private double skillMultiplier => 26.25; private double skillMultiplier => 23.25;
private double strainDecayBase => 0.15; private double strainDecayBase => 0.15;
private double strainValueOf(DifficultyHitObject current) 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; 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( // Rewarding angles, take the smaller velocity as base.
Math.Max(osuPrevious.JumpDistance - scale, 0) angleBonus = Math.Min(currVelocity, prevVelocity);
* Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2)
* Math.Max(osuCurrent.JumpDistance - scale, 0)); double wideAngleBonus = calcWideAngleBonus(currAngle);
aimStrain = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime); 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); aimStrain += angleBonus; // Add in angle bonus.
double travelDistanceExp = applyDiminishingExp(osuCurrent.TravelDistance);
return Math.Max( return aimStrain;
aimStrain + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold),
(Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / osuCurrent.StrainTime
);
} }
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 applyDiminishingExp(double val) => Math.Pow(val, 0.99);
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);

View File

@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Objects
set => StackHeightBindable.Value = value; 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; public double Radius => OBJECT_RADIUS * Scale;

View File

@ -79,6 +79,12 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary> /// </summary>
internal float LazyTravelDistance; internal float LazyTravelDistance;
/// <summary>
/// The time taken by the cursor upon completion of this <see cref="Slider"/> if it was hit
/// with as few movements as possible. This is set and used by difficulty calculation.
/// </summary>
internal double LazyTravelTime;
public IList<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>(); public IList<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
[JsonIgnore] [JsonIgnore]

View File

@ -8,6 +8,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects namespace osu.Game.Rulesets.Osu.Objects
{ {
@ -31,6 +32,8 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary> /// </summary>
public int MaximumBonusSpins { get; protected set; } = 1; public int MaximumBonusSpins { get; protected set; } = 1;
public override Vector2 StackOffset => Vector2.Zero;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -38,6 +38,15 @@ namespace osu.Game.Tests.Skins.IO
assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu); 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] [Test]
public Task TestSingleImportMatchingFilename() => runSkinTest(async osu => public Task TestSingleImportMatchingFilename() => runSkinTest(async osu =>
{ {
@ -199,21 +208,23 @@ namespace osu.Game.Tests.Skins.IO
return zipStream; 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(); var zipStream = new MemoryStream();
using var zip = ZipArchive.Create(); using var zip = ZipArchive.Create();
zip.AddEntry(iniFilename, generateSkinIni(name, author, makeUnique)); zip.AddEntry(iniFilename, generateSkinIni(name, author, makeUnique, includeSectionHeader));
zip.SaveTo(zipStream); zip.SaveTo(zipStream);
return 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 stream = new MemoryStream();
var writer = new StreamWriter(stream); var writer = new StreamWriter(stream);
writer.WriteLine("[General]"); if (includeSectionHeader)
writer.WriteLine("[General]");
writer.WriteLine($"Name: {name}"); writer.WriteLine($"Name: {name}");
writer.WriteLine($"Author: {author}"); writer.WriteLine($"Author: {author}");

View File

@ -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] [Test]
@ -290,6 +293,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => client.Room != null); 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] [Test]

View File

@ -45,11 +45,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
public void TestAddNullUser() public void TestAddUnresolvedUser()
{ {
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.User).Distinct().Count() == 1); AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().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); AddAssert("null user added", () => Client.Room.AsNonNull().Users.Count(u => u.User == null) == 1);
AddUntilStep("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.User).Distinct().Count() == 2); AddUntilStep("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.User).Distinct().Count() == 2);

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="DeepEqual" Version="2.0.0" /> <PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" /> <PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />

View File

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
</ItemGroup> </ItemGroup>

View File

@ -131,7 +131,7 @@ namespace osu.Game.Beatmaps
var localRulesetInfo = rulesetInfo as RulesetInfo; var localRulesetInfo = rulesetInfo as RulesetInfo;
// Difficulty can only be computed if the beatmap and ruleset are locally available. // 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). // 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)); return Task.FromResult(new StarDifficulty(beatmapInfo.StarRating, (beatmapInfo as IBeatmapOnlineInfo)?.MaxCombo ?? 0));

View File

@ -249,6 +249,23 @@ namespace osu.Game.Beatmaps
public IBindable<WeakReference<ArchiveDownloadRequest<BeatmapSetInfo>>> DownloadFailed => beatmapModelDownloader.DownloadFailed; public IBindable<WeakReference<ArchiveDownloadRequest<BeatmapSetInfo>>> 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) public bool Download(BeatmapSetInfo model, bool minimiseDownloadSize = false)
{ {
return beatmapModelDownloader.Download(model, minimiseDownloadSize); return beatmapModelDownloader.Download(model, minimiseDownloadSize);

View File

@ -60,8 +60,9 @@ namespace osu.Game.Beatmaps.Drawables
/// <param name="ruleset">The ruleset to show the difficulty with.</param> /// <param name="ruleset">The ruleset to show the difficulty with.</param>
/// <param name="mods">The mods to show the difficulty with.</param> /// <param name="mods">The mods to show the difficulty with.</param>
/// <param name="shouldShowTooltip">Whether to display a tooltip when hovered.</param> /// <param name="shouldShowTooltip">Whether to display a tooltip when hovered.</param>
public DifficultyIcon([NotNull] IBeatmapInfo beatmapInfo, [CanBeNull] IRulesetInfo ruleset, [CanBeNull] IReadOnlyList<Mod> mods, bool shouldShowTooltip = true) /// <param name="performBackgroundDifficultyLookup">Whether to perform difficulty lookup (including calculation if necessary).</param>
: this(beatmapInfo, shouldShowTooltip) public DifficultyIcon([NotNull] IBeatmapInfo beatmapInfo, [CanBeNull] IRulesetInfo ruleset, [CanBeNull] IReadOnlyList<Mod> mods, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true)
: this(beatmapInfo, shouldShowTooltip, performBackgroundDifficultyLookup)
{ {
this.ruleset = ruleset ?? beatmapInfo.Ruleset; this.ruleset = ruleset ?? beatmapInfo.Ruleset;
this.mods = mods ?? Array.Empty<Mod>(); this.mods = mods ?? Array.Empty<Mod>();

View File

@ -27,7 +27,7 @@ namespace osu.Game.Beatmaps.Formats
protected override void ParseStreamInto(LineBufferedReader stream, T output) protected override void ParseStreamInto(LineBufferedReader stream, T output)
{ {
Section section = Section.None; Section section = Section.General;
string line; string line;
@ -47,10 +47,7 @@ namespace osu.Game.Beatmaps.Formats
if (line.StartsWith('[') && line.EndsWith(']')) if (line.StartsWith('[') && line.EndsWith(']'))
{ {
if (!Enum.TryParse(line[1..^1], out section)) if (!Enum.TryParse(line[1..^1], out section))
{
Logger.Log($"Unknown section \"{line}\" in \"{output}\""); Logger.Log($"Unknown section \"{line}\" in \"{output}\"");
section = Section.None;
}
OnBeginNewSection(section); OnBeginNewSection(section);
continue; continue;
@ -148,7 +145,6 @@ namespace osu.Game.Beatmaps.Formats
protected enum Section protected enum Section
{ {
None,
General, General,
Editor, Editor,
Metadata, Metadata,

View File

@ -107,7 +107,8 @@ namespace osu.Game.Online.API
WebRequest = CreateWebRequest(); WebRequest = CreateWebRequest();
WebRequest.Failed += Fail; WebRequest.Failed += Fail;
WebRequest.AllowRetryOnTimeout = false; WebRequest.AllowRetryOnTimeout = false;
WebRequest.AddHeader("Authorization", $"Bearer {API.AccessToken}"); if (!string.IsNullOrEmpty(API.AccessToken))
WebRequest.AddHeader("Authorization", $"Bearer {API.AccessToken}");
if (isFailing) return; if (isFailing) return;

View File

@ -37,6 +37,7 @@ namespace osu.Game.Online.API.Requests.Responses
public DateTimeOffset Date { get; set; } public DateTimeOffset Date { get; set; }
[JsonProperty(@"beatmap")] [JsonProperty(@"beatmap")]
[CanBeNull]
public APIBeatmap Beatmap { get; set; } public APIBeatmap Beatmap { get; set; }
[JsonProperty("accuracy")] [JsonProperty("accuracy")]
@ -46,6 +47,7 @@ namespace osu.Game.Online.API.Requests.Responses
public double? PP { get; set; } public double? PP { get; set; }
[JsonProperty(@"beatmapset")] [JsonProperty(@"beatmapset")]
[CanBeNull]
public APIBeatmapSet BeatmapSet public APIBeatmapSet BeatmapSet
{ {
set set
@ -96,7 +98,7 @@ namespace osu.Game.Online.API.Requests.Responses
{ {
TotalScore = TotalScore, TotalScore = TotalScore,
MaxCombo = MaxCombo, MaxCombo = MaxCombo,
BeatmapInfo = Beatmap.ToBeatmapInfo(rulesets), BeatmapInfo = Beatmap?.ToBeatmapInfo(rulesets),
User = User, User = User,
Accuracy = Accuracy, Accuracy = Accuracy,
OnlineScoreID = OnlineID, OnlineScoreID = OnlineID,

View File

@ -148,6 +148,10 @@ namespace osu.Game.Online.Multiplayer
{ {
Room = joinedRoom; Room = joinedRoom;
APIRoom = room; APIRoom = room;
Debug.Assert(LocalUser != null);
addUserToAPIRoom(LocalUser);
foreach (var user in joinedRoom.Users) foreach (var user in joinedRoom.Users)
updateUserPlayingState(user.UserID, user.State); updateUserPlayingState(user.UserID, user.State);
@ -372,6 +376,8 @@ namespace osu.Game.Online.Multiplayer
Room.Users.Add(user); Room.Users.Add(user);
addUserToAPIRoom(user);
UserJoined?.Invoke(user); UserJoined?.Invoke(user);
RoomUpdated?.Invoke(); RoomUpdated?.Invoke();
}); });
@ -391,6 +397,18 @@ namespace osu.Game.Online.Multiplayer
return handleUserLeft(user, UserKicked); 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<MultiplayerRoomUser>? callback) private Task handleUserLeft(MultiplayerRoomUser user, Action<MultiplayerRoomUser>? callback)
{ {
if (Room == null) if (Room == null)
@ -404,6 +422,10 @@ namespace osu.Game.Online.Multiplayer
Room.Users.Remove(user); Room.Users.Remove(user);
PlayingUserIds.Remove(user.UserID); PlayingUserIds.Remove(user.UserID);
Debug.Assert(APIRoom != null);
APIRoom.RecentParticipants.RemoveAll(u => u.Id == user.UserID);
APIRoom.ParticipantCount.Value--;
callback?.Invoke(user); callback?.Invoke(user);
RoomUpdated?.Invoke(); RoomUpdated?.Invoke();
}, false); }, false);

View File

@ -53,7 +53,10 @@ namespace osu.Game.Online.Rooms
downloadTracker?.RemoveAndDisposeImmediately(); downloadTracker?.RemoveAndDisposeImmediately();
downloadTracker = new BeatmapDownloadTracker(item.NewValue.Beatmap.Value.BeatmapSet); downloadTracker = new BeatmapDownloadTracker(item.NewValue.Beatmap.Value.BeatmapSet);
downloadTracker.State.BindValueChanged(_ => updateAvailability());
AddInternal(downloadTracker);
downloadTracker.State.BindValueChanged(_ => updateAvailability(), true);
downloadTracker.Progress.BindValueChanged(_ => downloadTracker.Progress.BindValueChanged(_ =>
{ {
if (downloadTracker.State.Value != DownloadState.Downloading) 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. // we don't want to flood the network with this, so rate limit how often we send progress updates.
if (progressUpdate?.Completed != false) if (progressUpdate?.Completed != false)
progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500); progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500);
}); }, true);
AddInternal(downloadTracker);
}, true); }, true);
} }

View File

@ -5,6 +5,7 @@ using System.Net.Http;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Solo;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Online.Rooms namespace osu.Game.Online.Rooms
@ -14,14 +15,14 @@ namespace osu.Game.Online.Rooms
private readonly long scoreId; private readonly long scoreId;
private readonly long roomId; private readonly long roomId;
private readonly long playlistItemId; private readonly long playlistItemId;
private readonly ScoreInfo scoreInfo; private readonly SubmittableScore score;
public SubmitRoomScoreRequest(long scoreId, long roomId, long playlistItemId, ScoreInfo scoreInfo) public SubmitRoomScoreRequest(long scoreId, long roomId, long playlistItemId, ScoreInfo scoreInfo)
{ {
this.scoreId = scoreId; this.scoreId = scoreId;
this.roomId = roomId; this.roomId = roomId;
this.playlistItemId = playlistItemId; this.playlistItemId = playlistItemId;
this.scoreInfo = scoreInfo; score = new SubmittableScore(scoreInfo);
} }
protected override WebRequest CreateWebRequest() protected override WebRequest CreateWebRequest()
@ -31,7 +32,7 @@ namespace osu.Game.Online.Rooms
req.ContentType = "application/json"; req.ContentType = "application/json";
req.Method = HttpMethod.Put; req.Method = HttpMethod.Put;
req.AddRaw(JsonConvert.SerializeObject(scoreInfo, new JsonSerializerSettings req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings
{ {
ReferenceLoopHandling = ReferenceLoopHandling.Ignore ReferenceLoopHandling = ReferenceLoopHandling.Ignore
})); }));

View File

@ -86,7 +86,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
break; break;
default: default:
beatmaps.Download(new BeatmapSetInfo { OnlineBeatmapSetID = beatmapSet.OnlineID }, noVideoSetting.Value); beatmaps.Download(beatmapSet, noVideoSetting.Value);
break; break;
} }
}; };

View File

@ -108,7 +108,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
return; return;
} }
beatmaps.Download(new BeatmapSetInfo { OnlineBeatmapSetID = beatmapSet.OnlineID }, noVideo); beatmaps.Download(beatmapSet, noVideo);
}; };
localUser.BindTo(api.LocalUser); localUser.BindTo(api.LocalUser);

View File

@ -91,7 +91,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
{ {
new OsuSpriteText new OsuSpriteText
{ {
Text = $"{Score.Beatmap.DifficultyName}", Text = $"{Score.Beatmap?.DifficultyName}",
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
Colour = colours.Yellow Colour = colours.Yellow
}, },

View File

@ -58,6 +58,11 @@ namespace osu.Game.Rulesets.Difficulty
return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate); return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
} }
/// <summary>
/// Calculates the difficulty of the beatmap and returns a set of <see cref="TimedDifficultyAttributes"/> representing the difficulty at every relevant time value in the beatmap.
/// </summary>
/// <param name="mods">The mods that should be applied to the beatmap.</param>
/// <returns>The set of <see cref="TimedDifficultyAttributes"/>.</returns>
public List<TimedDifficultyAttributes> CalculateTimed(params Mod[] mods) public List<TimedDifficultyAttributes> CalculateTimed(params Mod[] mods)
{ {
preProcess(mods); preProcess(mods);
@ -77,7 +82,7 @@ namespace osu.Game.Rulesets.Difficulty
foreach (var skill in skills) foreach (var skill in skills)
skill.ProcessInternal(hitObject); 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; return attribs;

View File

@ -11,9 +11,21 @@ namespace osu.Game.Rulesets.Difficulty
/// </summary> /// </summary>
public class TimedDifficultyAttributes : IComparable<TimedDifficultyAttributes> public class TimedDifficultyAttributes : IComparable<TimedDifficultyAttributes>
{ {
/// <summary>
/// The non-clock-adjusted time value at which the attributes take effect.
/// </summary>
public readonly double Time; public readonly double Time;
/// <summary>
/// The attributes.
/// </summary>
public readonly DifficultyAttributes Attributes; public readonly DifficultyAttributes Attributes;
/// <summary>
/// Creates new <see cref="TimedDifficultyAttributes"/>.
/// </summary>
/// <param name="time">The non-clock-adjusted time value at which the attributes take effect.</param>
/// <param name="attributes">The attributes.</param>
public TimedDifficultyAttributes(double time, DifficultyAttributes attributes) public TimedDifficultyAttributes(double time, DifficultyAttributes attributes)
{ {
Time = time; Time = time;

View File

@ -105,7 +105,7 @@ namespace osu.Game.Screens.OnlinePlay
private void refresh() 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.Clear();
beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString(), null, text => beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString(), null, text =>

View File

@ -194,68 +194,61 @@ namespace osu.Game.Skinning
string nameLine = @$"Name: {item.Name}"; string nameLine = @$"Name: {item.Name}";
string authorLine = @$"Author: {item.Creator}"; 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)); var existingFile = item.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase));
if (existingFile != null) if (existingFile == null)
{ {
List<string> outputLines = new List<string>(); // In the case a skin doesn't have a skin.ini yet, let's create one.
writeNewSkinIni();
return;
}
bool addedName = false; using (Stream stream = new MemoryStream())
bool addedAuthor = false; {
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
using (var stream = Files.Storage.GetStream(existingFile.FileInfo.StoragePath))
using (var sr = new StreamReader(stream))
{ {
string line; using (var existingStream = Files.Storage.GetStream(existingFile.FileInfo.StoragePath))
using (var sr = new StreamReader(existingStream))
while ((line = sr.ReadLine()) != null)
{ {
if (line.StartsWith(@"Name:", StringComparison.Ordinal)) string line;
{ while ((line = sr.ReadLine()) != null)
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)
sw.WriteLine(line); 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 (Stream stream = new MemoryStream())
{ {
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{ {
sw.WriteLine(@"[General]"); foreach (string line in newLines)
sw.WriteLine(nameLine); sw.WriteLine(line);
sw.WriteLine(authorLine);
sw.WriteLine(@"Version: latest");
} }
AddFile(item, stream, @"skin.ini"); 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) protected override Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
{ {
var instance = GetSkin(model); var instance = GetSkin(model);

View File

@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
return roomUser; 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) private void addUser(MultiplayerRoomUser user)
{ {

View File

@ -14,11 +14,11 @@ namespace osu.Game.Tests.Visual
/// A special user ID which <see cref="ComputeValueAsync"/> would return a <see langword="null"/> <see cref="User"/> for. /// A special user ID which <see cref="ComputeValueAsync"/> would return a <see langword="null"/> <see cref="User"/> for.
/// As a simulation to what a regular <see cref="UserLookupCache"/> would return in the case of failing to fetch the user. /// As a simulation to what a regular <see cref="UserLookupCache"/> would return in the case of failing to fetch the user.
/// </summary> /// </summary>
public const int NULL_USER_ID = -1; public const int UNRESOLVED_USER_ID = -1;
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default) protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
{ {
if (lookup == NULL_USER_ID) if (lookup == UNRESOLVED_USER_ID)
return Task.FromResult((User)null); return Task.FromResult((User)null);
return Task.FromResult(new User return Task.FromResult(new User

View File

@ -23,9 +23,9 @@
<PackageReference Include="HtmlAgilityPack" Version="1.11.37" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.37" />
<PackageReference Include="Humanizer" Version="2.11.10" /> <PackageReference Include="Humanizer" Version="2.11.10" />
<PackageReference Include="MessagePack" Version="2.3.85" /> <PackageReference Include="MessagePack" Version="2.3.85" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.10" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.11" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.10" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.11" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.10" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
@ -38,8 +38,8 @@
<PackageReference Include="Realm" Version="10.6.0" /> <PackageReference Include="Realm" Version="10.6.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1029.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.1029.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" />
<PackageReference Include="Sentry" Version="3.9.4" /> <PackageReference Include="Sentry" Version="3.10.0" />
<PackageReference Include="SharpCompress" Version="0.29.0" /> <PackageReference Include="SharpCompress" Version="0.30.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="TagLibSharp" Version="2.2.0" /> <PackageReference Include="TagLibSharp" Version="2.2.0" />

View File

@ -94,7 +94,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1029.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.1029.0" />
<PackageReference Include="SharpCompress" Version="0.28.3" /> <PackageReference Include="SharpCompress" Version="0.30.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />