1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 10:07:52 +08:00

Merge branch 'master' into leaderboard-friend-highlight

This commit is contained in:
Dean Herbert 2023-09-20 14:14:08 +09:00 committed by GitHub
commit 1927b524db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
155 changed files with 3682 additions and 1448 deletions

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.904.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.914.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -26,7 +26,7 @@
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.1.4.20" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />

View File

@ -7,9 +7,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.4" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.8" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<ItemGroup>

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
AddAssert("default slider velocity", () => lastObject.SliderVelocityBindable.IsDefault);
AddAssert("default slider velocity", () => lastObject.SliderVelocityMultiplierBindable.IsDefault);
}
[Test]
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addPlacementSteps(times, positions);
addPathCheckStep(times, positions);
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityBindable.IsDefault);
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityMultiplierBindable.IsDefault);
}
[Test]

View File

@ -108,11 +108,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
double[] times = { 100, 300 };
float[] positions = { 200, 300 };
addBlueprintStep(times, positions);
AddAssert("default slider velocity", () => hitObject.SliderVelocityBindable.IsDefault);
AddAssert("default slider velocity", () => hitObject.SliderVelocityMultiplierBindable.IsDefault);
addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 400);
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityBindable.IsDefault);
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
}
[Test]

View File

@ -0,0 +1,157 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual.Gameplay;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
public partial class TestSceneScoring : ScoringTestScene
{
public TestSceneScoring()
: base(supportsNonPerfectJudgements: false)
{
}
private Bindable<double> scoreMultiplier { get; } = new BindableDouble
{
Default = 4,
Value = 4
};
protected override IBeatmap CreateBeatmap(int maxCombo)
{
var beatmap = new CatchBeatmap();
for (int i = 0; i < maxCombo; ++i)
beatmap.HitObjects.Add(new Fruit());
return beatmap;
}
protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } };
protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new CatchProcessorBasedScoringAlgorithm(beatmap, mode);
[Test]
public void TestBasicScenarios()
{
AddStep("set up score multiplier", () =>
{
scoreMultiplier.BindValueChanged(_ => Rerun());
});
AddStep("set max combo to 100", () => MaxCombo.Value = 100);
AddStep("set perfect score", () =>
{
NonPerfectLocations.Clear();
MissLocations.Clear();
});
AddStep("set score with misses", () =>
{
NonPerfectLocations.Clear();
MissLocations.Clear();
MissLocations.AddRange(new[] { 24d, 49 });
});
AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier);
}
private const int base_great = 300;
private class ScoreV1 : IScoringAlgorithm
{
private int currentCombo;
public BindableDouble ScoreMultiplier { get; } = new BindableDouble();
public void ApplyHit() => applyHitV1(base_great);
public void ApplyNonPerfect() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements.");
public void ApplyMiss() => applyHitV1(0);
private void applyHitV1(int baseScore)
{
if (baseScore == 0)
{
currentCombo = 0;
return;
}
TotalScore += baseScore;
// combo multiplier
// ReSharper disable once PossibleLossOfFraction
TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * ScoreMultiplier.Value));
currentCombo++;
}
public long TotalScore { get; private set; }
}
private class ScoreV2 : IScoringAlgorithm
{
private int currentCombo;
private double comboPortion;
private readonly double comboPortionMax;
private const double combo_base = 4;
private const int combo_cap = 200;
public ScoreV2(int maxCombo)
{
for (int i = 0; i < maxCombo; i++)
ApplyHit();
comboPortionMax = comboPortion;
currentCombo = 0;
comboPortion = 0;
}
public void ApplyHit() => applyHitV2(base_great);
public void ApplyNonPerfect() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements.");
private void applyHitV2(int baseScore)
{
comboPortion += baseScore * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(combo_cap, combo_base));
}
public void ApplyMiss()
{
currentCombo = 0;
}
public long TotalScore
=> (int)Math.Round(1000000 * comboPortion / comboPortionMax); // vast simplification, as we're not doing ticks here.
}
private class CatchProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
{
public CatchProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
: base(beatmap, mode)
{
}
protected override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor();
protected override JudgementResult CreatePerfectJudgementResult() => new CatchJudgementResult(new Fruit(), new CatchJudgement()) { Type = HitResult.Great };
protected override JudgementResult CreateNonPerfectJudgementResult() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements.");
protected override JudgementResult CreateMissJudgementResult() => new CatchJudgementResult(new Fruit(), new CatchJudgement()) { Type = HitResult.Miss };
}
}
}

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
}.Yield();
case IHasDuration endTime:

View File

@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdateHitObjectFromPath(JuiceStream hitObject)
{
// The SV setting may need to be changed for the current path.
var svBindable = hitObject.SliderVelocityBindable;
var svBindable = hitObject.SliderVelocityMultiplierBindable;
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
double requiredVelocity = path.ComputeRequiredVelocity();

View File

@ -28,17 +28,17 @@ namespace osu.Game.Rulesets.Catch.Objects
public int RepeatCount { get; set; }
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
};
public double SliderVelocity
public double SliderVelocityMultiplier
{
get => SliderVelocityBindable.Value;
set => SliderVelocityBindable.Value = value;
get => SliderVelocityMultiplierBindable.Value;
set => SliderVelocityMultiplierBindable.Value = value;
}
[JsonIgnore]
@ -48,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects
private double tickDistanceFactor;
[JsonIgnore]
public double Velocity => velocityFactor * SliderVelocity;
public double Velocity => velocityFactor * SliderVelocityMultiplier;
[JsonIgnore]
public double TickDistance => tickDistanceFactor * SliderVelocity;
public double TickDistance => tickDistanceFactor * SliderVelocityMultiplier;
/// <summary>
/// The length of one span of this <see cref="JuiceStream"/>.

View File

@ -0,0 +1,175 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual.Gameplay;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
public partial class TestSceneScoring : ScoringTestScene
{
protected override IBeatmap CreateBeatmap(int maxCombo)
{
var beatmap = new ManiaBeatmap(new StageDefinition(5));
for (int i = 0; i < maxCombo; ++i)
beatmap.HitObjects.Add(new Note());
return beatmap;
}
protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1(MaxCombo.Value);
protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new ManiaProcessorBasedScoringAlgorithm(beatmap, mode);
[Test]
public void TestBasicScenarios()
{
AddStep("set max combo to 100", () => MaxCombo.Value = 100);
AddStep("set perfect score", () =>
{
NonPerfectLocations.Clear();
MissLocations.Clear();
});
AddStep("set score with misses", () =>
{
NonPerfectLocations.Clear();
MissLocations.Clear();
MissLocations.AddRange(new[] { 24d, 49 });
});
AddStep("set score with misses and OKs", () =>
{
NonPerfectLocations.Clear();
MissLocations.Clear();
NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 });
MissLocations.AddRange(new[] { 24d, 49 });
});
}
private class ScoreV1 : IScoringAlgorithm
{
private int currentCombo;
private double comboAddition = 100;
private double totalScoreDouble;
private readonly double scoreMultiplier;
public ScoreV1(int maxCombo)
{
scoreMultiplier = 500000d / maxCombo;
}
public void ApplyHit() => applyHitV1(320, add => add + 2, 32);
public void ApplyNonPerfect() => applyHitV1(100, add => add - 24, 8);
public void ApplyMiss() => applyHitV1(0, _ => -56, 0);
private void applyHitV1(int scoreIncrease, Func<double, double> comboAdditionFunc, int delta)
{
comboAddition = comboAdditionFunc(comboAddition);
if (currentCombo != 0 && currentCombo % 384 == 0)
comboAddition = 100;
comboAddition = Math.Max(0, Math.Min(comboAddition, 100));
double scoreIncreaseD = Math.Sqrt(comboAddition) * delta * scoreMultiplier / 320;
TotalScore = (long)totalScoreDouble;
scoreIncreaseD += scoreIncrease * scoreMultiplier / 320;
scoreIncrease = (int)scoreIncreaseD;
TotalScore += scoreIncrease;
totalScoreDouble += scoreIncreaseD;
if (scoreIncrease > 0)
currentCombo++;
}
public long TotalScore { get; private set; }
}
private class ScoreV2 : IScoringAlgorithm
{
private int currentCombo;
private double comboPortion;
private double currentBaseScore;
private double maxBaseScore;
private int currentHits;
private readonly double comboPortionMax;
private readonly int maxCombo;
private const double combo_base = 4;
public ScoreV2(int maxCombo)
{
this.maxCombo = maxCombo;
for (int i = 0; i < this.maxCombo; i++)
ApplyHit();
comboPortionMax = comboPortion;
currentCombo = 0;
comboPortion = 0;
currentBaseScore = 0;
maxBaseScore = 0;
currentHits = 0;
}
public void ApplyHit() => applyHitV2(305, 300);
public void ApplyNonPerfect() => applyHitV2(100, 100);
private void applyHitV2(int hitValue, int baseHitValue)
{
maxBaseScore += 305;
currentBaseScore += hitValue;
comboPortion += baseHitValue * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(400, combo_base));
currentHits++;
}
public void ApplyMiss()
{
currentHits++;
maxBaseScore += 305;
currentCombo = 0;
}
public long TotalScore
{
get
{
float accuracy = (float)(currentBaseScore / maxBaseScore);
return (int)Math.Round
(
200000 * comboPortion / comboPortionMax +
800000 * Math.Pow(accuracy, 2 + 2 * accuracy) * ((double)currentHits / maxCombo)
);
}
}
}
private class ManiaProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
{
public ManiaProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
: base(beatmap, mode)
{
}
protected override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
protected override JudgementResult CreatePerfectJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Perfect };
protected override JudgementResult CreateNonPerfectJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Ok };
protected override JudgementResult CreateMissJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Miss };
}
}
}

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -10,10 +10,11 @@ using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
@ -50,10 +51,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
double beatLength;
if (hitObject.LegacyBpmMultiplier.HasValue)
beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value;
else if (hitObject is IHasSliderVelocity hasSliderVelocity)
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
if (hitObject is IHasSliderVelocity hasSliderVelocity)
beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, ManiaRuleset.SHORT_NAME);
else
beatLength = timingPoint.BeatLength;

View File

@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
Lookup = lookup;
ColumnIndex = columnIndex;
}
public override string ToString() => $"[{nameof(ManiaSkinConfigurationLookup)} lookup:{Lookup} col:{ColumnIndex}]";
}
}

View File

@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
new PathControlPoint(new Vector2(0, 6.25f))
}),
RepeatCount = 1,
SliderVelocity = 10
SliderVelocityMultiplier = 10
}
}
},

View File

@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase("basic")]
[TestCase("colinear-perfect-curve")]
[TestCase("slider-ticks")]
[TestCase("slider-ticks-edge-case")]
[TestCase("repeat-slider")]
[TestCase("uneven-repeat-slider")]
[TestCase("old-stacking")]

View File

@ -549,12 +549,151 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
addJudgementAssert(hitObjects[1], HitResult.Miss);
// the slider head of the first slider prevents the second slider's head from being hit, so the judgement offset should be very late.
// this is not strictly done by the hit policy implementation itself (see `OsuModClassic.blockInputToUnderlyingObjects()`),
// this is not strictly done by the hit policy implementation itself (see `OsuModClassic.blockInputToObjectsUnderSliderHead()`),
// but we're testing this here anyways to just keep everything related to input handling and note lock in one place.
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, referenceHitWindows.WindowFor(HitResult.Meh));
addClickActionAssert(0, ClickAction.Hit);
}
[Test]
public void TestOverlappingSlidersDontBlockEachOtherWhenFullyJudged()
{
const double time_first_slider = 1000;
const double time_second_slider = 1600;
Vector2 positionFirstSlider = new Vector2(100, 50);
Vector2 positionSecondSlider = new Vector2(100, 80);
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
var hitObjects = new List<OsuHitObject>
{
new Slider
{
StartTime = time_first_slider,
Position = positionFirstSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
},
new Slider
{
StartTime = time_second_slider,
Position = positionSecondSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint },
// this frame doesn't do anything on lazer, but is REQUIRED for correct playback on stable,
// because stable during replay playback only updates game state _when it encounters a replay frame_
new OsuReplayFrame { Time = 1250, Position = midpoint },
new OsuReplayFrame { Time = time_second_slider + 50, Position = midpoint, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_second_slider + 75, Position = midpoint },
});
addJudgementAssert(hitObjects[0], HitResult.Ok);
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
addJudgementAssert(hitObjects[1], HitResult.Ok);
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, 50);
addClickActionAssert(0, ClickAction.Hit);
addClickActionAssert(1, ClickAction.Hit);
}
[Test]
public void TestOverlappingHitCirclesDontBlockEachOtherWhenBothVisible()
{
const double time_first_circle = 1000;
const double time_second_circle = 1200;
Vector2 positionFirstCircle = new Vector2(100);
Vector2 positionSecondCircle = new Vector2(120);
var midpoint = (positionFirstCircle + positionSecondCircle) / 2;
var hitObjects = new List<OsuHitObject>
{
new HitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle,
},
new HitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle,
},
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle, Position = midpoint, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle + 25, Position = midpoint },
new OsuReplayFrame { Time = time_first_circle + 50, Position = midpoint, Actions = { OsuAction.RightButton } },
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementOffsetAssert(hitObjects[0], 0);
addJudgementAssert(hitObjects[1], HitResult.Meh);
addJudgementOffsetAssert(hitObjects[1], -150);
}
[Test]
public void TestOverlappingHitCirclesDontBlockEachOtherWhenFullyFadedOut()
{
const double time_first_circle = 1000;
const double time_second_circle = 1200;
const double time_third_circle = 1400;
Vector2 positionFirstCircle = new Vector2(100);
Vector2 positionSecondCircle = new Vector2(200);
var hitObjects = new List<OsuHitObject>
{
new HitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle,
},
new HitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle,
},
new HitCircle
{
StartTime = time_third_circle,
Position = positionFirstCircle,
},
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle + 50, Position = positionFirstCircle },
new OsuReplayFrame { Time = time_second_circle - 50, Position = positionSecondCircle },
new OsuReplayFrame { Time = time_second_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_second_circle + 50, Position = positionSecondCircle },
new OsuReplayFrame { Time = time_third_circle - 50, Position = positionFirstCircle },
new OsuReplayFrame { Time = time_third_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_third_circle + 50, Position = positionFirstCircle },
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementOffsetAssert(hitObjects[0], 0);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementOffsetAssert(hitObjects[1], 0);
addJudgementAssert(hitObjects[2], HitResult.Great);
addJudgementOffsetAssert(hitObjects[2], 0);
}
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",

View File

@ -0,0 +1,176 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual.Gameplay;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public partial class TestSceneScoring : ScoringTestScene
{
private Bindable<double> scoreMultiplier { get; } = new BindableDouble
{
Default = 4,
Value = 4
};
protected override IBeatmap CreateBeatmap(int maxCombo)
{
var beatmap = new OsuBeatmap();
for (int i = 0; i < maxCombo; i++)
beatmap.HitObjects.Add(new HitCircle());
return beatmap;
}
protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } };
protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new OsuProcessorBasedScoringAlgorithm(beatmap, mode);
[Test]
public void TestBasicScenarios()
{
AddStep("set up score multiplier", () =>
{
scoreMultiplier.BindValueChanged(_ => Rerun());
});
AddStep("set max combo to 100", () => MaxCombo.Value = 100);
AddStep("set perfect score", () =>
{
NonPerfectLocations.Clear();
MissLocations.Clear();
});
AddStep("set score with misses", () =>
{
NonPerfectLocations.Clear();
MissLocations.Clear();
MissLocations.AddRange(new[] { 24d, 49 });
});
AddStep("set score with misses and OKs", () =>
{
NonPerfectLocations.Clear();
MissLocations.Clear();
NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 });
MissLocations.AddRange(new[] { 24d, 49 });
});
AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier);
}
private const int base_great = 300;
private const int base_ok = 100;
private class ScoreV1 : IScoringAlgorithm
{
private int currentCombo;
public BindableDouble ScoreMultiplier { get; } = new BindableDouble();
public void ApplyHit() => applyHitV1(base_great);
public void ApplyNonPerfect() => applyHitV1(base_ok);
public void ApplyMiss() => applyHitV1(0);
private void applyHitV1(int baseScore)
{
if (baseScore == 0)
{
currentCombo = 0;
return;
}
TotalScore += baseScore;
// combo multiplier
// ReSharper disable once PossibleLossOfFraction
TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * ScoreMultiplier.Value));
currentCombo++;
}
public long TotalScore { get; private set; }
}
private class ScoreV2 : IScoringAlgorithm
{
private int currentCombo;
private double comboPortion;
private double currentBaseScore;
private double maxBaseScore;
private int currentHits;
private readonly double comboPortionMax;
private readonly int maxCombo;
public ScoreV2(int maxCombo)
{
this.maxCombo = maxCombo;
for (int i = 0; i < this.maxCombo; i++)
ApplyHit();
comboPortionMax = comboPortion;
currentCombo = 0;
comboPortion = 0;
currentBaseScore = 0;
maxBaseScore = 0;
currentHits = 0;
}
public void ApplyHit() => applyHitV2(base_great);
public void ApplyNonPerfect() => applyHitV2(base_ok);
private void applyHitV2(int baseScore)
{
maxBaseScore += base_great;
currentBaseScore += baseScore;
comboPortion += baseScore * (1 + ++currentCombo / 10.0);
currentHits++;
}
public void ApplyMiss()
{
currentHits++;
maxBaseScore += base_great;
currentCombo = 0;
}
public long TotalScore
{
get
{
double accuracy = currentBaseScore / maxBaseScore;
return (int)Math.Round
(
700000 * comboPortion / comboPortionMax +
300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
);
}
}
}
private class OsuProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
{
public OsuProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
: base(beatmap, mode)
{
}
protected override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor();
protected override JudgementResult CreatePerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great };
protected override JudgementResult CreateNonPerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok };
protected override JudgementResult CreateMissJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss };
}
}
}

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Game.Audio;
@ -19,7 +20,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
@ -35,16 +35,24 @@ namespace osu.Game.Rulesets.Osu.Tests
{
private int depthIndex;
private readonly BindableBool snakingIn = new BindableBool();
private readonly BindableBool snakingOut = new BindableBool();
private readonly BindableBool snakingIn = new BindableBool(true);
private readonly BindableBool snakingOut = new BindableBool(true);
[SetUpSteps]
public void SetUpSteps()
private float progressToHit;
protected override void LoadComplete()
{
AddToggleStep("toggle snaking", v =>
base.LoadComplete();
AddToggleStep("disable snaking", v =>
{
snakingIn.Value = v;
snakingOut.Value = v;
snakingIn.Value = !v;
snakingOut.Value = !v;
});
AddSliderStep("hit at", 0f, 1f, 0f, v =>
{
progressToHit = v;
});
}
@ -56,6 +64,18 @@ namespace osu.Game.Rulesets.Osu.Tests
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
}
protected override void Update()
{
base.Update();
foreach (var slider in this.ChildrenOfType<DrawableSlider>())
{
double completionProgress = Math.Clamp((Time.Current - slider.HitObject.StartTime) / slider.HitObject.Duration, 0, 1);
if (completionProgress > progressToHit && !slider.IsHit)
slider.HeadCircle.HitArea.Hit();
}
}
[Test]
public void TestVariousSliders()
{
@ -206,7 +226,7 @@ namespace osu.Game.Rulesets.Osu.Tests
StackHeight = 10
};
return createDrawable(slider, 2, 2);
return createDrawable(slider, 2);
}
private Drawable testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats);
@ -229,6 +249,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
SliderVelocityMultiplier = speedMultiplier,
StartTime = Time.Current + time_offset,
Position = new Vector2(0, -(distance / 2)),
Path = new SliderPath(PathType.PerfectCurve, new[]
@ -240,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Tests
StackHeight = stackHeight
};
return createDrawable(slider, circleSize, speedMultiplier);
return createDrawable(slider, circleSize);
}
private Drawable testPerfect(int repeats = 0)
@ -258,7 +279,7 @@ namespace osu.Game.Rulesets.Osu.Tests
RepeatCount = repeats,
};
return createDrawable(slider, 2, 3);
return createDrawable(slider, 2);
}
private Drawable testLinear(int repeats = 0) => createLinear(repeats);
@ -281,7 +302,7 @@ namespace osu.Game.Rulesets.Osu.Tests
RepeatCount = repeats,
};
return createDrawable(slider, 2, 3);
return createDrawable(slider, 2);
}
private Drawable testBezier(int repeats = 0) => createBezier(repeats);
@ -303,7 +324,7 @@ namespace osu.Game.Rulesets.Osu.Tests
RepeatCount = repeats,
};
return createDrawable(slider, 2, 3);
return createDrawable(slider, 2);
}
private Drawable testLinearOverlapping(int repeats = 0) => createOverlapping(repeats);
@ -326,7 +347,7 @@ namespace osu.Game.Rulesets.Osu.Tests
RepeatCount = repeats,
};
return createDrawable(slider, 2, 3);
return createDrawable(slider, 2);
}
private Drawable testCatmull(int repeats = 0) => createCatmull(repeats);
@ -352,15 +373,12 @@ namespace osu.Game.Rulesets.Osu.Tests
NodeSamples = repeatSamples
};
return createDrawable(slider, 3, 1);
return createDrawable(slider, 3);
}
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
private Drawable createDrawable(Slider slider, float circleSize)
{
var cpi = new LegacyControlPointInfo();
cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier });
slider.ApplyDefaults(cpi, new BeatmapDifficulty
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty
{
CircleSize = circleSize,
SliderTickRate = 3

View File

@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
SliderVelocity = velocity,
SliderVelocityMultiplier = velocity,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,

View File

@ -349,7 +349,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
SliderVelocity = 0.1f,
SliderVelocityMultiplier = 0.1f,
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
@ -33,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public partial class TestSceneSliderSnaking : TestSceneOsuPlayer
{
[Resolved]
private AudioManager audioManager { get; set; }
private AudioManager audioManager { get; set; } = null!;
protected override bool Autoplay => autoplay;
private bool autoplay;
@ -41,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Tests
private readonly BindableBool snakingIn = new BindableBool();
private readonly BindableBool snakingOut = new BindableBool();
private IBeatmap beatmap;
private IBeatmap beatmap = null!;
private const double duration_of_span = 3605;
private const double fade_in_modifier = -1200;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[BackgroundDependencyLoader]
@ -57,15 +55,8 @@ namespace osu.Game.Rulesets.Osu.Tests
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
}
private Slider slider;
private DrawableSlider drawableSlider;
[SetUp]
public void Setup() => Schedule(() =>
{
slider = null;
drawableSlider = null;
});
private Slider slider = null!;
private DrawableSlider? drawableSlider;
protected override bool HasCustomSteps => true;
@ -135,9 +126,9 @@ namespace osu.Game.Rulesets.Osu.Tests
}
[Test]
public void TestRepeatArrowDoesNotMoveWhenHit()
public void TestRepeatArrowDoesNotMove([Values] bool useAutoplay)
{
AddStep("enable autoplay", () => autoplay = true);
AddStep($"set autoplay to {useAutoplay}", () => autoplay = useAutoplay);
setSnaking(true);
CreateTest();
// repeat might have a chance to update its position depending on where in the frame its hit,
@ -145,21 +136,12 @@ namespace osu.Game.Rulesets.Osu.Tests
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame);
}
[Test]
public void TestRepeatArrowMovesWhenNotHit()
{
AddStep("disable autoplay", () => autoplay = false);
setSnaking(true);
CreateTest();
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased);
}
private void retrieveSlider(int index)
{
AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
addSeekStep(() => slider.StartTime);
AddUntilStep("retrieve drawable slider", () =>
(drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
(drawableSlider = (DrawableSlider?)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
}
private void addEnsureSnakingInSteps(Func<double> startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionIncreased);
@ -179,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private Func<double> timeAtRepeat(Func<double> startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex;
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? getSliderStart : getSliderEnd;
private List<Vector2> getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
private List<Vector2> getSliderCurve() => ((PlaySliderBody)drawableSlider!.Body.Drawable).CurrentCurve;
private Vector2 getSliderStart() => getSliderCurve().First();
private Vector2 getSliderEnd() => getSliderCurve().Last();

View File

@ -451,7 +451,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
SliderVelocity = 0.1f;
SliderVelocityMultiplier = 0.1f;
DefaultsApplied += _ =>
{

View File

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1,
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1,
}.Yield();
case IHasDuration endTimeData:

View File

@ -85,9 +85,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
BeginPlacement();
double? nearestSliderVelocity = (editorBeatmap.HitObjects
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocity;
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier;
HitObject.SliderVelocity = nearestSliderVelocity ?? 1;
HitObject.SliderVelocityMultiplier = nearestSliderVelocity ?? 1;
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.

View File

@ -25,9 +25,6 @@ namespace osu.Game.Rulesets.Osu.Mods
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);
[SettingSource("No slider head movement", "Pins slider heads at their starting position, regardless of time.")]
public Bindable<bool> NoSliderHeadMovement { get; } = new BindableBool(true);
[SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")]
public Bindable<bool> ClassicNoteLock { get; } = new BindableBool(true);
@ -71,12 +68,11 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (obj)
{
case DrawableSliderHead head:
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
if (FadeHitCircleEarly.Value && !usingHiddenFading)
applyEarlyFading(head);
if (ClassicNoteLock.Value)
blockInputToUnderlyingObjects(head);
blockInputToObjectsUnderSliderHead(head);
break;
@ -88,25 +84,23 @@ namespace osu.Game.Rulesets.Osu.Mods
if (FadeHitCircleEarly.Value && !usingHiddenFading)
applyEarlyFading(circle);
if (ClassicNoteLock.Value)
blockInputToUnderlyingObjects(circle);
break;
}
}
/// <summary>
/// On stable, hitcircles that have already been hit block input from reaching objects that may be underneath them.
/// On stable, slider heads that have already been hit block input from reaching objects that may be underneath them
/// until the sliders they're part of have been fully judged.
/// The purpose of this method is to restore that behaviour.
/// In order to avoid introducing yet another confusing config option, this behaviour is roped into the general notion of "note lock".
/// </summary>
private static void blockInputToUnderlyingObjects(DrawableHitCircle circle)
private static void blockInputToObjectsUnderSliderHead(DrawableSliderHead slider)
{
var oldHitAction = circle.HitArea.Hit;
circle.HitArea.Hit = () =>
var oldHitAction = slider.HitArea.Hit;
slider.HitArea.Hit = () =>
{
oldHitAction?.Invoke();
return true;
return !slider.DrawableSlider.AllJudged;
};
}

View File

@ -2,13 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDifficultyAdjust : ModDifficultyAdjust
public partial class OsuModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
@ -20,12 +26,13 @@ namespace osu.Game.Rulesets.Osu.Mods
ReadCurrentFromDifficulty = diff => diff.CircleSize,
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(ApproachRateSettingsControl))]
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 10,
ExtendedMinValue = -10,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
};
@ -53,5 +60,34 @@ namespace osu.Game.Rulesets.Osu.Mods
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
}
private partial class ApproachRateSettingsControl : DifficultyAdjustSettingsControl
{
protected override RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) =>
new ApproachRateSlider
{
RelativeSizeAxes = Axes.X,
Current = current,
KeyboardStep = 0.1f,
};
/// <summary>
/// A slider bar with more detailed approach rate info for its given value
/// </summary>
public partial class ApproachRateSlider : RoundedSliderBar<float>
{
public override LocalisableString TooltipText =>
(Current as BindableNumber<float>)?.MinValue < 0
? $"{base.TooltipText} ({getPreemptTime(Current.Value):0} ms)"
: base.TooltipText;
private double getPreemptTime(float approachRate)
{
var hitCircle = new HitCircle();
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { ApproachRate = approachRate });
return hitCircle.TimePreempt;
}
}
}
}
}

View File

@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Mods
ComboOffset = original.ComboOffset;
LegacyLastTickOffset = original.LegacyLastTickOffset;
TickDistanceMultiplier = original.TickDistanceMultiplier;
SliderVelocity = original.SliderVelocity;
SliderVelocityMultiplier = original.SliderVelocityMultiplier;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)

View File

@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
Ball.UpdateProgress(completionProgress);
SliderBody?.UpdateProgress(completionProgress);
SliderBody?.UpdateProgress(HeadCircle.IsHit ? completionProgress : 0);
foreach (DrawableHitObject hitObject in NestedHitObjects)
{
@ -317,7 +317,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
switch (state)
{
case ArmedState.Hit:
if (SliderBody?.SnakingOut.Value == true)
if (HeadCircle.IsHit && SliderBody?.SnakingOut.Value == true)
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
break;
}

View File

@ -3,11 +3,9 @@
#nullable disable
using System;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
@ -24,12 +22,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
/// <summary>
/// Makes this <see cref="DrawableSliderHead"/> track the follow circle when the start time is reached.
/// If <c>false</c>, this <see cref="DrawableSliderHead"/> will be pinned to its initial position in the slider.
/// </summary>
public bool TrackFollowCircle = true;
private readonly IBindable<int> pathVersion = new Bindable<int>();
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;
@ -64,23 +56,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
CheckHittable = (d, t, r) => DrawableSlider.CheckHittable?.Invoke(d, t, r) ?? ClickAction.Hit;
}
protected override void Update()
{
base.Update();
Debug.Assert(Slider != null);
Debug.Assert(HitObject != null);
if (TrackFollowCircle)
{
double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1);
//todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
if (!IsHit)
Position = Slider.CurvePositionAt(completionProgress);
}
}
protected override HitResult ResultFor(double timeOffset)
{
Debug.Assert(HitObject != null);

View File

@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
private int wholeSpins;
private int completedFullSpins;
private void updateBonusScore()
{
@ -295,14 +295,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
int spins = (int)(Result.RateAdjustedRotation / 360);
if (spins < wholeSpins)
if (spins < completedFullSpins)
{
// rewinding, silently handle
wholeSpins = spins;
completedFullSpins = spins;
return;
}
while (wholeSpins != spins)
while (completedFullSpins != spins)
{
var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
@ -312,10 +312,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
tick.TriggerResult(true);
if (tick is DrawableSpinnerBonusTick)
gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequired);
gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequiredForBonus);
}
wholeSpins++;
completedFullSpins++;
}
}
}

View File

@ -16,6 +16,7 @@ using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
@ -113,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public double SpanDuration => Duration / this.SpanCount();
/// <summary>
/// Velocity of this <see cref="Slider"/>.
/// The computed velocity of this <see cref="Slider"/>. This is the amount of path distance travelled in 1 ms.
/// </summary>
public double Velocity { get; private set; }
@ -134,17 +135,16 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
public bool OnlyJudgeNestedObjects = true;
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
};
public double SliderVelocity
public double SliderVelocityMultiplier
{
get => SliderVelocityBindable.Value;
set => SliderVelocityBindable.Value = value;
get => SliderVelocityMultiplierBindable.Value;
set => SliderVelocityMultiplierBindable.Value = value;
}
public bool GenerateTicks { get; set; } = true;
@ -167,9 +167,11 @@ namespace osu.Game.Rulesets.Osu.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * SliderVelocity;
Velocity = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, OsuRuleset.SHORT_NAME);
// WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier`
// for backwards compatibility reasons (intentionally introducing floating point errors to match stable).
double scoringDistance = Velocity * timingPoint.BeatLength;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
}

View File

@ -31,6 +31,16 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
public int SpinsRequired { get; protected set; } = 1;
/// <summary>
/// The number of spins required to start receiving bonus score. The first bonus is awarded on this spin count.
/// </summary>
public int SpinsRequiredForBonus => SpinsRequired + bonus_spins_gap;
/// <summary>
/// The gap between spinner completion and the first bonus-awarding spin.
/// </summary>
private const int bonus_spins_gap = 2;
/// <summary>
/// Number of spins available to give bonus, beyond <see cref="SpinsRequired"/>.
/// </summary>
@ -42,25 +52,20 @@ namespace osu.Game.Rulesets.Osu.Objects
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
// spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being.
const double stable_matching_fudge = 0.6;
// close to 477rpm
const double maximum_rotations_per_second = 8;
const double maximum_rotations_per_second = 477f / 60f;
double secondsDuration = Duration / 1000;
double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 1.5, 2.5, 3.75);
SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond);
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration) - bonus_spins_gap;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects(cancellationToken);
int totalSpins = MaximumBonusSpins + SpinsRequired;
int totalSpins = MaximumBonusSpins + SpinsRequired + bonus_spins_gap;
for (int i = 0; i < totalSpins; i++)
{
@ -68,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Objects
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired
AddNested(i < SpinsRequiredForBonus
? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
: new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration, Samples = new[] { CreateHitSampleInfo("spinnerbonus") } });
}

View File

@ -0,0 +1,39 @@
{
"Mappings": [
{
"StartTime": 7493.0,
"Objects": [
{
"StartTime": 7493.0,
"EndTime": 7493.0,
"X": 130.0,
"Y": 232.0,
"StackOffset": {
"X": 0.0,
"Y": 0.0
}
},
{
"StartTime": 7817.0,
"EndTime": 7817.0,
"X": 30.9946651,
"Y": 208.5157,
"StackOffset": {
"X": 0.0,
"Y": 0.0
}
},
{
"StartTime": 7843.0,
"EndTime": 7843.0,
"X": 33.7820168,
"Y": 208.9957,
"StackOffset": {
"X": 0.0,
"Y": 0.0
}
}
]
}
]
}

View File

@ -0,0 +1,31 @@
osu file format v14
[General]
StackLeniency: 0.7
[Difficulty]
HPDrainRate:3
CircleSize:3.4
OverallDifficulty:4
ApproachRate:5.5
SliderMultiplier:1.1
SliderTickRate:1
[Events]
//Background and Video events
0,0,"aa.jpg",0,0
//Break Periods
//Storyboard Layer 0 (Background)
//Storyboard Layer 1 (Fail)
//Storyboard Layer 2 (Pass)
//Storyboard Layer 3 (Foreground)
//Storyboard Layer 4 (Overlay)
//Storyboard Sound Samples
[TimingPoints]
6967,350.877192982456,6,2,1,55,1,0
6967,-100,3,2,1,55,0,0
7493,-111.111111111111,3,2,1,55,0,0
[HitObjects]
130,232,7493,6,0,P|78:218|28:208,1,101.82149697876,0|0,3:2|2:0,2:0:0:0:

View File

@ -46,22 +46,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
BorderSize = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1;
BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
drawableObject.HitObjectApplied += onHitObjectApplied;
}
private void onHitObjectApplied(DrawableHitObject obj)
{
var drawableSlider = (DrawableSlider)obj;
if (drawableSlider.HitObject == null)
return;
// When not tracking the follow circle, unbind from the config and forcefully disable snaking out - it looks better that way.
if (!drawableSlider.HeadCircle.TrackFollowCircle)
{
SnakingOut.UnbindFrom(configSnakingOut);
SnakingOut.Value = false;
}
}
protected virtual Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) =>

View File

@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
if (time - part.Time >= 1)
continue;
vertexBatch.Add(new TexturedTrailVertex(renderer)
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomLeft,
@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time
});
vertexBatch.Add(new TexturedTrailVertex(renderer)
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomRight,
@ -304,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time
});
vertexBatch.Add(new TexturedTrailVertex(renderer)
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopRight,
@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time
});
vertexBatch.Add(new TexturedTrailVertex(renderer)
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopLeft,
@ -362,22 +362,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
[VertexMember(1, VertexAttribPointerType.Float)]
public float Time;
[VertexMember(1, VertexAttribPointerType.Int)]
private readonly int maskingIndex;
public TexturedTrailVertex(IRenderer renderer)
{
this = default;
maskingIndex = renderer.CurrentMaskingIndex;
}
public bool Equals(TexturedTrailVertex other)
{
return Position.Equals(other.Position)
&& TexturePosition.Equals(other.TexturePosition)
&& Colour.Equals(other.Colour)
&& Time.Equals(other.Time)
&& maskingIndex == other.maskingIndex;
&& Time.Equals(other.Time);
}
}
}

View File

@ -0,0 +1,185 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Tests.Visual.Gameplay;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
public partial class TestSceneScoring : ScoringTestScene
{
private Bindable<double> scoreMultiplier { get; } = new BindableDouble
{
Default = 4,
Value = 4
};
protected override IBeatmap CreateBeatmap(int maxCombo)
{
var beatmap = new TaikoBeatmap();
for (int i = 0; i < maxCombo; ++i)
beatmap.HitObjects.Add(new Hit());
return beatmap;
}
protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } };
protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new TaikoProcessorBasedScoringAlgorithm(beatmap, mode);
[Test]
public void TestBasicScenarios()
{
AddStep("set up score multiplier", () =>
{
scoreMultiplier.BindValueChanged(_ => Rerun());
});
AddStep("set max combo to 100", () => MaxCombo.Value = 100);
AddStep("set perfect score", () =>
{
NonPerfectLocations.Clear();
MissLocations.Clear();
});
AddStep("set score with misses", () =>
{
NonPerfectLocations.Clear();
MissLocations.Clear();
MissLocations.AddRange(new[] { 24d, 49 });
});
AddStep("set score with misses and OKs", () =>
{
NonPerfectLocations.Clear();
MissLocations.Clear();
NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 });
MissLocations.AddRange(new[] { 24d, 49 });
});
AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier);
}
private const int base_great = 300;
private const int base_ok = 150;
private class ScoreV1 : IScoringAlgorithm
{
private int currentCombo;
public BindableDouble ScoreMultiplier { get; } = new BindableDouble();
public void ApplyHit() => applyHitV1(base_great);
public void ApplyNonPerfect() => applyHitV1(base_ok);
public void ApplyMiss() => applyHitV1(0);
private void applyHitV1(int baseScore)
{
if (baseScore == 0)
{
currentCombo = 0;
return;
}
TotalScore += baseScore;
// combo multiplier
// ReSharper disable once PossibleLossOfFraction
TotalScore += (int)((baseScore / 35) * 2 * (ScoreMultiplier.Value + 1)) * (Math.Min(100, currentCombo) / 10);
currentCombo++;
}
public long TotalScore { get; private set; }
}
private class ScoreV2 : IScoringAlgorithm
{
private int currentCombo;
private double comboPortion;
private double currentBaseScore;
private double maxBaseScore;
private int currentHits;
private readonly double comboPortionMax;
private readonly int maxCombo;
private const double combo_base = 4;
public ScoreV2(int maxCombo)
{
this.maxCombo = maxCombo;
for (int i = 0; i < this.maxCombo; i++)
ApplyHit();
comboPortionMax = comboPortion;
currentCombo = 0;
comboPortion = 0;
currentBaseScore = 0;
maxBaseScore = 0;
currentHits = 0;
}
public void ApplyHit() => applyHitV2(base_great);
public void ApplyNonPerfect() => applyHitV2(base_ok);
private void applyHitV2(int baseScore)
{
maxBaseScore += base_great;
currentBaseScore += baseScore;
currentHits++;
// `base_great` is INTENTIONALLY used above here instead of `baseScore`
// see `BaseHitValue` override in `ScoreChangeTaiko` on stable
comboPortion += base_great * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(400, combo_base));
}
public void ApplyMiss()
{
currentHits++;
maxBaseScore += base_great;
currentCombo = 0;
}
public long TotalScore
{
get
{
double accuracy = currentBaseScore / maxBaseScore;
return (int)Math.Round
(
250000 * comboPortion / comboPortionMax +
750000 * Math.Pow(accuracy, 3.6) * ((double)currentHits / maxCombo)
);
}
}
}
private class TaikoProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
{
public TaikoProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
: base(beatmap, mode)
{
}
protected override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor();
protected override JudgementResult CreatePerfectJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great };
protected override JudgementResult CreateNonPerfectJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok };
protected override JudgementResult CreateMissJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss };
}
}
}

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -14,6 +14,7 @@ using JetBrains.Annotations;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Objects.Legacy;
namespace osu.Game.Rulesets.Taiko.Beatmaps
{
@ -64,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue;
double nextScrollSpeed = hasSliderVelocity.SliderVelocity;
double nextScrollSpeed = hasSliderVelocity.SliderVelocityMultiplier;
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
@ -186,10 +187,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
double beatLength;
if (obj.LegacyBpmMultiplier.HasValue)
beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value;
else if (obj is IHasSliderVelocity hasSliderVelocity)
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
if (obj is IHasSliderVelocity hasSliderVelocity)
beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, TaikoRuleset.SHORT_NAME);
else
beatLength = timingPoint.BeatLength;

View File

@ -621,6 +621,38 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
[Test]
public void TestInvalidBankDefaultsToNormal()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("invalid-bank.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
assertObjectHasBanks(hitObjects[0], HitSampleInfo.BANK_DRUM);
assertObjectHasBanks(hitObjects[1], HitSampleInfo.BANK_NORMAL);
assertObjectHasBanks(hitObjects[2], HitSampleInfo.BANK_SOFT);
assertObjectHasBanks(hitObjects[3], HitSampleInfo.BANK_DRUM);
assertObjectHasBanks(hitObjects[4], HitSampleInfo.BANK_NORMAL);
assertObjectHasBanks(hitObjects[5], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_DRUM);
assertObjectHasBanks(hitObjects[6], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL);
assertObjectHasBanks(hitObjects[7], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_SOFT);
assertObjectHasBanks(hitObjects[8], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_DRUM);
assertObjectHasBanks(hitObjects[9], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL);
}
void assertObjectHasBanks(HitObject hitObject, string normalBank, string? additionsBank = null)
{
Assert.AreEqual(normalBank, hitObject.Samples[0].Bank);
if (additionsBank != null)
Assert.AreEqual(additionsBank, hitObject.Samples[1].Bank);
}
}
[Test]
public void TestFallbackDecoderForCorruptedHeader()
{
@ -1024,10 +1056,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1));
Assert.That(controlPoints.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(1));
#pragma warning disable 618
Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(2000)).GenerateTicks, Is.False);
Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(3000)).GenerateTicks, Is.True);
#pragma warning restore 618
Assert.That(controlPoints.DifficultyPointAt(2000).GenerateTicks, Is.False);
Assert.That(controlPoints.DifficultyPointAt(3000).GenerateTicks, Is.True);
}
}
}

View File

@ -32,8 +32,26 @@ namespace osu.Game.Tests.Chat
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a gopher://really-old-protocol we don't support." });
Assert.AreEqual(result.Content, result.DisplayContent);
Assert.AreEqual(1, result.Links.Count);
Assert.AreEqual("gopher://really-old-protocol", result.Links[0].Url);
Assert.AreEqual(0, result.Links.Count);
}
[Test]
public void TestFakeProtocolLink()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a osunotarealprotocol://completely-made-up-protocol we don't support." });
Assert.AreEqual(result.Content, result.DisplayContent);
Assert.AreEqual(0, result.Links.Count);
}
[Test]
public void TestSupportedProtocolLinkParsing()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "forgotspacehttps://dev.ppy.sh joinmyosump://12345 jointheosu://chan/#english" });
Assert.AreEqual("https://dev.ppy.sh", result.Links[0].Url);
Assert.AreEqual("osump://12345", result.Links[1].Url);
Assert.AreEqual("osu://chan/#english", result.Links[2].Url);
}
[Test]

View File

@ -78,7 +78,7 @@ namespace osu.Game.Tests.Editing
{
assertSnapDistance(100, new Slider
{
SliderVelocity = multiplier
SliderVelocityMultiplier = multiplier
}, false);
}
@ -88,7 +88,7 @@ namespace osu.Game.Tests.Editing
{
assertSnapDistance(100 * multiplier, new Slider
{
SliderVelocity = multiplier
SliderVelocityMultiplier = multiplier
}, true);
}
@ -112,7 +112,7 @@ namespace osu.Game.Tests.Editing
var referenceObject = new Slider
{
SliderVelocity = slider_velocity
SliderVelocityMultiplier = slider_velocity
};
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);

View File

@ -13,7 +13,7 @@ layout(location = 4) out mediump vec2 v_BlendRange;
void main(void)
{
// Transform from screen space to masking space.
highp vec3 maskingPos = g_MaskingInfo.ToMaskingSpace * vec3(m_Position, 1.0);
highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
v_MaskingPosition = maskingPos.xy / maskingPos.z;
v_Colour = m_Colour;

View File

@ -0,0 +1,19 @@
osu file format v14
[General]
SampleSet: Normal
[TimingPoints]
0,500,4,3,0,100,1,0
[HitObjects]
256,192,1000,5,0,0:0:0:0:
256,192,2000,1,0,1:0:0:0:
256,192,3000,1,0,2:0:0:0:
256,192,4000,1,0,3:0:0:0:
256,192,5000,1,0,42:0:0:0:
256,192,6000,5,4,0:0:0:0:
256,192,7000,1,4,0:1:0:0:
256,192,8000,1,4,0:2:0:0:
256,192,9000,1,4,0:3:0:0:
256,192,10000,1,4,0:42:0:0:

View File

@ -260,6 +260,12 @@ namespace osu.Game.Tests.Visual.Beatmaps
AddStep($"set {scheme} scheme", () => Child = createContent(scheme, creationFunc));
}
[Test]
public void TestNano()
{
createTestCase(beatmapSetInfo => new BeatmapCardNano(beatmapSetInfo));
}
[Test]
public void TestNormal()
{

View File

@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Editing
new PathControlPoint(new Vector2(100, 0))
}
},
SliderVelocity = 2
SliderVelocityMultiplier = 2
});
});
}
@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("unify slider velocity", () =>
{
foreach (var h in EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>())
h.SliderVelocity = 1.5;
h.SliderVelocityMultiplier = 1.5;
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
@ -194,7 +194,7 @@ namespace osu.Game.Tests.Visual.Editing
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocity == velocity;
return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocityMultiplier == velocity;
});
}
}

View File

@ -67,6 +67,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private Player loadPlayerFor(RulesetInfo rulesetInfo)
{
// if a player screen is present already, we must exit that before loading another one,
// otherwise it'll crash on SpectatorClient.BeginPlaying being called while client is in "playing" state already.
if (Stack.CurrentScreen is Player)
Stack.Exit();
Ruleset.Value = rulesetInfo;
var ruleset = rulesetInfo.CreateInstance();

View File

@ -1,499 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring.Legacy;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneScoring : OsuTestScene
{
private GraphContainer graphs = null!;
private SettingsSlider<int> sliderMaxCombo = null!;
private FillFlowContainer legend = null!;
[Test]
public void TestBasic()
{
AddStep("setup tests", () =>
{
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
graphs = new GraphContainer
{
RelativeSizeAxes = Axes.Both,
},
},
new Drawable[]
{
legend = new FillFlowContainer
{
Padding = new MarginPadding(20),
Direction = FillDirection.Full,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
},
new Drawable[]
{
new FillFlowContainer
{
Padding = new MarginPadding(20),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Children = new Drawable[]
{
sliderMaxCombo = new SettingsSlider<int>
{
Width = 0.5f,
TransferValueOnCommit = true,
Current = new BindableInt(1024)
{
MinValue = 96,
MaxValue = 8192,
},
LabelText = "max combo",
},
new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
Width = 0.5f,
AutoSizeAxes = Axes.Y,
Text = $"Left click to add miss\nRight click to add OK/{base_ok}"
}
}
},
},
}
}
};
sliderMaxCombo.Current.BindValueChanged(_ => rerun());
graphs.MissLocations.BindCollectionChanged((_, __) => rerun());
graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun());
graphs.MaxCombo.BindTo(sliderMaxCombo.Current);
rerun();
});
}
private const int base_great = 300;
private const int base_ok = 100;
private void rerun()
{
graphs.Clear();
legend.Clear();
runForProcessor("lazer-standardised", Color4.YellowGreen, new OsuScoreProcessor(), ScoringMode.Standardised);
runForProcessor("lazer-classic", Color4.MediumPurple, new OsuScoreProcessor(), ScoringMode.Classic);
runScoreV1();
runScoreV2();
}
private void runScoreV1()
{
int totalScore = 0;
int currentCombo = 0;
void applyHitV1(int baseScore)
{
if (baseScore == 0)
{
currentCombo = 0;
return;
}
const float score_multiplier = 1;
totalScore += baseScore;
// combo multiplier
// ReSharper disable once PossibleLossOfFraction
totalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier));
currentCombo++;
}
runForAlgorithm("ScoreV1 (classic)", Color4.Purple,
() => applyHitV1(base_great),
() => applyHitV1(base_ok),
() => applyHitV1(0),
() =>
{
// Arbitrary value chosen towards the upper range.
const double score_multiplier = 4;
return (int)(totalScore * score_multiplier);
});
}
private void runScoreV2()
{
int maxCombo = sliderMaxCombo.Current.Value;
int currentCombo = 0;
double comboPortion = 0;
double currentBaseScore = 0;
double maxBaseScore = 0;
int currentHits = 0;
for (int i = 0; i < maxCombo; i++)
applyHitV2(base_great);
double comboPortionMax = comboPortion;
currentCombo = 0;
comboPortion = 0;
currentBaseScore = 0;
maxBaseScore = 0;
currentHits = 0;
void applyHitV2(int baseScore)
{
maxBaseScore += base_great;
currentBaseScore += baseScore;
comboPortion += baseScore * (1 + ++currentCombo / 10.0);
currentHits++;
}
runForAlgorithm("ScoreV2", Color4.OrangeRed,
() => applyHitV2(base_great),
() => applyHitV2(base_ok),
() =>
{
currentHits++;
maxBaseScore += base_great;
currentCombo = 0;
}, () =>
{
double accuracy = currentBaseScore / maxBaseScore;
return (int)Math.Round
(
700000 * comboPortion / comboPortionMax +
300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
);
});
}
private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode)
{
int maxCombo = sliderMaxCombo.Current.Value;
var beatmap = new OsuBeatmap();
for (int i = 0; i < maxCombo; i++)
beatmap.HitObjects.Add(new HitCircle());
processor.ApplyBeatmap(beatmap);
runForAlgorithm(name, colour,
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }),
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }),
() => processor.GetDisplayScore(mode));
}
private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func<long> getTotalScore)
{
int maxCombo = sliderMaxCombo.Current.Value;
List<float> results = new List<float>();
for (int i = 0; i < maxCombo; i++)
{
if (graphs.MissLocations.Contains(i))
applyMiss();
else if (graphs.NonPerfectLocations.Contains(i))
applyNonPerfect();
else
applyHit();
results.Add(getTotalScore());
}
graphs.Add(new LineGraph
{
Name = name,
RelativeSizeAxes = Axes.Both,
LineColour = colour,
Values = results
});
legend.Add(new OsuSpriteText
{
Colour = colour,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Text = $"{FontAwesome.Solid.Circle.Icon} {name}"
});
legend.Add(new OsuSpriteText
{
Colour = colour,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Text = $"final score {getTotalScore():#,0}"
});
}
}
public partial class GraphContainer : Container, IHasCustomTooltip<IEnumerable<LineGraph>>
{
public readonly BindableList<double> MissLocations = new BindableList<double>();
public readonly BindableList<double> NonPerfectLocations = new BindableList<double>();
public Bindable<int> MaxCombo = new Bindable<int>();
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
private readonly Box hoverLine;
private readonly Container missLines;
private readonly Container verticalGridLines;
public int CurrentHoverCombo { get; private set; }
public GraphContainer()
{
InternalChild = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.1f),
RelativeSizeAxes = Axes.Both,
},
verticalGridLines = new Container
{
RelativeSizeAxes = Axes.Both,
},
hoverLine = new Box
{
Colour = Color4.Yellow,
RelativeSizeAxes = Axes.Y,
Origin = Anchor.TopCentre,
Alpha = 0,
Width = 1,
},
missLines = new Container
{
Alpha = 0.6f,
RelativeSizeAxes = Axes.Both,
},
Content,
}
};
MissLocations.BindCollectionChanged((_, _) => updateMissLocations());
NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations());
MaxCombo.BindValueChanged(_ =>
{
updateMissLocations();
updateVerticalGridLines();
}, true);
}
private void updateVerticalGridLines()
{
verticalGridLines.Clear();
for (int i = 0; i < MaxCombo.Value; i++)
{
if (i % 100 == 0)
{
verticalGridLines.AddRange(new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.2f),
Origin = Anchor.TopCentre,
Width = 1,
RelativeSizeAxes = Axes.Y,
RelativePositionAxes = Axes.X,
X = (float)i / MaxCombo.Value,
},
new OsuSpriteText
{
RelativePositionAxes = Axes.X,
X = (float)i / MaxCombo.Value,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Text = $"{i:#,0}",
Rotation = -30,
Y = -20,
}
});
}
}
}
private void updateMissLocations()
{
missLines.Clear();
foreach (int miss in MissLocations)
{
missLines.Add(new Box
{
Colour = Color4.Red,
Origin = Anchor.TopCentre,
Width = 1,
RelativeSizeAxes = Axes.Y,
RelativePositionAxes = Axes.X,
X = (float)miss / MaxCombo.Value,
});
}
foreach (int miss in NonPerfectLocations)
{
missLines.Add(new Box
{
Colour = Color4.Orange,
Origin = Anchor.TopCentre,
Width = 1,
RelativeSizeAxes = Axes.Y,
RelativePositionAxes = Axes.X,
X = (float)miss / MaxCombo.Value,
});
}
}
protected override bool OnHover(HoverEvent e)
{
hoverLine.Show();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
hoverLine.Hide();
base.OnHoverLost(e);
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value);
hoverLine.X = e.MousePosition.X;
return base.OnMouseMove(e);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Left)
MissLocations.Add(CurrentHoverCombo);
else
NonPerfectLocations.Add(CurrentHoverCombo);
return true;
}
private GraphTooltip? tooltip;
public ITooltip<IEnumerable<LineGraph>> GetCustomTooltip() => tooltip ??= new GraphTooltip(this);
public IEnumerable<LineGraph> TooltipContent => Content.OfType<LineGraph>();
public partial class GraphTooltip : CompositeDrawable, ITooltip<IEnumerable<LineGraph>>
{
private readonly GraphContainer graphContainer;
private readonly OsuTextFlowContainer textFlow;
public GraphTooltip(GraphContainer graphContainer)
{
this.graphContainer = graphContainer;
AutoSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 10;
InternalChildren = new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.15f),
RelativeSizeAxes = Axes.Both,
},
textFlow = new OsuTextFlowContainer
{
Colour = Color4.White,
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding(10),
}
};
}
private int? lastContentCombo;
public void SetContent(IEnumerable<LineGraph> content)
{
int relevantCombo = graphContainer.CurrentHoverCombo;
if (lastContentCombo == relevantCombo)
return;
lastContentCombo = relevantCombo;
textFlow.Clear();
textFlow.AddParagraph($"At combo {relevantCombo}:");
foreach (var graph in content)
{
float valueAtHover = graph.Values.ElementAt(relevantCombo);
float ofTotal = valueAtHover / graph.Values.Last();
textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour);
}
}
public void Move(Vector2 pos) => this.MoveTo(pos);
}
}
}

View File

@ -64,6 +64,9 @@ namespace osu.Game.Tests.Visual.Online
addMessageWithChecks("test!");
addMessageWithChecks("dev.ppy.sh!");
addMessageWithChecks("https://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
addMessageWithChecks("http://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
addMessageWithChecks("forgothttps://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
addMessageWithChecks("forgothttp://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp);
addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.OpenWiki);
addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External);
@ -84,9 +87,11 @@ namespace osu.Game.Tests.Visual.Online
addMessageWithChecks("feels important", 0, true, true);
addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External);
addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
addMessageWithChecks("Join my multiplayer gameosump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
addMessageWithChecks($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", 1, expectedActions: LinkAction.OpenChannel);
addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel);
addMessageWithChecks($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel);
addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel });
addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel);
addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20");

View File

@ -0,0 +1,97 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using System.Net;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneReplayMissingBeatmap : OsuGameTestScene
{
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[Test]
public void TestSceneMissingBeatmapWithOnlineAvailable()
{
var beatmap = new APIBeatmap
{
OnlineBeatmapSetID = 173612,
BeatmapSet = new APIBeatmapSet
{
Title = "FREEDOM Dive",
Artist = "xi",
Covers = new BeatmapSetOnlineCovers
{
Card = "https://assets.ppy.sh/beatmaps/173612/covers/card@2x.jpg"
},
OnlineID = 173612
}
};
setupBeatmapResponse(beatmap);
AddStep("import score", () =>
{
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
{
var importTask = new ImportTask(resourceStream, "replay.osr");
Game.ScoreManager.Import(new[] { importTask });
}
});
AddUntilStep("Replay missing notification show", () => Game.Notifications.ChildrenOfType<MissingBeatmapNotification>().Any());
}
[Test]
public void TestSceneMissingBeatmapWithOnlineUnavailable()
{
setupFailedResponse();
AddStep("import score", () =>
{
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
{
var importTask = new ImportTask(resourceStream, "replay.osr");
Game.ScoreManager.Import(new[] { importTask });
}
});
AddUntilStep("Replay missing notification not show", () => !Game.Notifications.ChildrenOfType<MissingBeatmapNotification>().Any());
}
private void setupBeatmapResponse(APIBeatmap b)
=> AddStep("setup response", () =>
{
dummyAPI.HandleRequest = request =>
{
if (request is GetBeatmapRequest getBeatmapRequest)
{
getBeatmapRequest.TriggerSuccess(b);
return true;
}
return false;
};
});
private void setupFailedResponse()
=> AddStep("setup failed response", () =>
{
dummyAPI.HandleRequest = request =>
{
request.TriggerFailure(new WebException());
return true;
};
});
}
}

View File

@ -110,5 +110,31 @@ namespace osu.Game.Tests.Visual.Online
}
}, new OsuRuleset().RulesetInfo));
}
[Test]
public void TestPreviousUsernames()
{
AddStep("Show user w/ previous usernames", () => header.User.Value = new UserProfileData(new APIUser
{
Id = 727,
Username = "SomeoneIndecisive",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
Groups = new[]
{
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
},
Statistics = new UserStatistics
{
IsRanked = false,
// web will sometimes return non-empty rank history even for unranked users.
RankHistory = new APIRankHistory
{
Mode = @"osu",
Data = Enumerable.Range(2345, 85).ToArray()
},
},
PreviousUsernames = new[] { "tsrk.", "quoicoubeh", "apagnan", "epita" }
}, new OsuRuleset().RulesetInfo));
}
}
}

View File

@ -134,14 +134,16 @@ namespace osu.Game.Tests.Visual.Online
{
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
Description = "Outstanding help by being a voluntary test subject.",
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg",
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor-new@2x.png",
ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor-new.png",
Url = "https://osu.ppy.sh/wiki/en/People/Community_Contributors",
},
new Badge
{
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
Description = "Badge without a url.",
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg",
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor@2x.png",
ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor.png",
},
},
Title = "osu!volunteer",

View File

@ -5,20 +5,29 @@ using System;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile.Header.Components;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public partial class TestSceneUserProfilePreviousUsernames : OsuTestScene
public partial class TestSceneUserProfilePreviousUsernamesDisplay : OsuTestScene
{
private PreviousUsernames container = null!;
private PreviousUsernamesDisplay container = null!;
private OverlayColourProvider colourProvider = null!;
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = container = new PreviousUsernames
colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
Child = new DependencyProvidingContainer
{
Child = container = new PreviousUsernamesDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
CachedDependencies = new (Type, object)[] { (typeof(OverlayColourProvider), colourProvider) },
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};

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 System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Database;
using osu.Game.Overlays;
using osu.Game.Tests.Scores.IO;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneMissingBeatmapNotification : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
[BackgroundDependencyLoader]
private void load()
{
Child = new Container
{
Width = 280,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = new MissingBeatmapNotification(CreateAPIBeatmapSet(Ruleset.Value).Beatmaps.First(), new ImportScoreTest.TestArchiveReader(), "deadbeef")
};
}
}
}

View File

@ -0,0 +1,131 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneModEffectPreviewPanel : OsuTestScene
{
[Cached(typeof(BeatmapDifficultyCache))]
private TestBeatmapDifficultyCache difficultyCache = new TestBeatmapDifficultyCache();
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
private Container content = null!;
protected override Container<Drawable> Content => content;
private BeatmapAttributesDisplay panel = null!;
[BackgroundDependencyLoader]
private void load()
{
base.Content.AddRange(new Drawable[]
{
difficultyCache,
content = new Container
{
RelativeSizeAxes = Axes.Both
}
});
}
[Test]
public void TestDisplay()
{
OsuModDifficultyAdjust difficultyAdjust = new OsuModDifficultyAdjust();
OsuModDoubleTime doubleTime = new OsuModDoubleTime();
AddStep("create display", () => Child = panel = new BeatmapAttributesDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
AddStep("set beatmap", () =>
{
var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo)
{
BeatmapInfo =
{
BPM = 120
}
};
Ruleset.Value = beatmap.BeatmapInfo.Ruleset;
panel.BeatmapInfo.Value = beatmap.BeatmapInfo;
});
AddSliderStep("change star rating", 0, 10d, 5, stars =>
{
if (panel.IsNotNull())
previewStarRating(stars);
});
AddStep("preview ridiculously high SR", () => previewStarRating(1234));
AddStep("add DA to mods", () => SelectedMods.Value = new[] { difficultyAdjust });
AddSliderStep("change AR", 0, 10f, 5, ar =>
{
if (panel.IsNotNull())
difficultyAdjust.ApproachRate.Value = ar;
});
AddSliderStep("change CS", 0, 10f, 5, cs =>
{
if (panel.IsNotNull())
difficultyAdjust.CircleSize.Value = cs;
});
AddSliderStep("change HP", 0, 10f, 5, hp =>
{
if (panel.IsNotNull())
difficultyAdjust.DrainRate.Value = hp;
});
AddSliderStep("change OD", 0, 10f, 5, od =>
{
if (panel.IsNotNull())
difficultyAdjust.OverallDifficulty.Value = od;
});
AddStep("add DT to mods", () => SelectedMods.Value = new Mod[] { difficultyAdjust, doubleTime });
AddSliderStep("change rate", 1.01d, 2d, 1.5d, rate =>
{
if (panel.IsNotNull())
doubleTime.SpeedChange.Value = rate;
});
AddToggleStep("toggle collapsed", collapsed => panel.Collapsed.Value = collapsed);
}
private void previewStarRating(double stars)
{
difficultyCache.Difficulty = new StarDifficulty(stars, 0);
panel.BeatmapInfo.TriggerChange();
}
private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache
{
public StarDifficulty? Difficulty { get; set; }
public override Task<StarDifficulty?> GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable<Mod>? mods = null,
CancellationToken cancellationToken = default)
=> Task.FromResult(Difficulty);
}
}
}

View File

@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("clear contents", Clear);
AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
AddStep("reset mods", () => SelectedMods.SetDefault());
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo));
AddStep("set up presets", () =>
{
Realm.Write(r =>
@ -92,6 +93,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Beatmap = Beatmap.Value,
SelectedMods = { BindTarget = SelectedMods }
});
waitForColumnLoad();
@ -113,7 +115,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
@ -128,7 +130,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
@ -785,7 +787,7 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.MoveMouseTo(this.ChildrenOfType<ModPresetPanel>().Single(preset => preset.Preset.Value.Name == "Half Time 0.5x"));
InputManager.Click(MouseButton.Left);
});
AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.5));
AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.5));
// this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation,
// it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
@ -794,7 +796,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick());
AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModSettingsArea>().Single()
.ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick());
AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.7));
AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.7));
}
private void waitForColumnLoad() => AddUntilStep("all column content loaded",

View File

@ -1,68 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneModsEffectDisplay : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Resolved]
private OsuColour colours { get; set; } = null!;
[Test]
public void TestModsEffectDisplay()
{
TestDisplay testDisplay = null!;
Box background = null!;
AddStep("add display", () =>
{
Add(testDisplay = new TestDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
var boxes = testDisplay.ChildrenOfType<Box>();
background = boxes.First();
});
AddStep("set value to default", () => testDisplay.Current.Value = 50);
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == Color4.White && background.Colour == colourProvider.Background3);
AddStep("set value to less", () => testDisplay.Current.Value = 40);
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == colourProvider.Background5 && background.Colour == colours.ForModType(ModType.DifficultyReduction));
AddStep("set value to bigger", () => testDisplay.Current.Value = 60);
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == colourProvider.Background5 && background.Colour == colours.ForModType(ModType.DifficultyIncrease));
}
private partial class TestDisplay : ModsEffectDisplay
{
public Container<Drawable> Container => Content;
protected override LocalisableString Label => "Test display";
public TestDisplay()
{
Current.Default = 50;
}
}
}
}

View File

@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
@ -12,17 +11,17 @@ using osu.Game.Overlays.Mods;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneDifficultyMultiplierDisplay : OsuTestScene
public partial class TestSceneScoreMultiplierDisplay : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Test]
public void TestDifficultyMultiplierDisplay()
public void TestBasic()
{
DifficultyMultiplierDisplay multiplierDisplay = null;
ScoreMultiplierDisplay multiplierDisplay = null!;
AddStep("create content", () => Child = multiplierDisplay = new DifficultyMultiplierDisplay
AddStep("create content", () => Child = multiplierDisplay = new ScoreMultiplierDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
@ -34,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddSliderStep("set multiplier", 0, 2, 1d, multiplier =>
{
if (multiplierDisplay != null)
if (multiplierDisplay.IsNotNull())
multiplierDisplay.Current.Value = multiplier;
});
}

View File

@ -2,10 +2,10 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="DeepEqual" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="Moq" Version="4.18.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -4,9 +4,9 @@
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -126,7 +126,7 @@ namespace osu.Game.Tournament.Screens.MapPool
if (CurrentMatch.Value == null || CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2)
return;
// if bans have already been placed, beatmap changes result in a selection being made autoamtically
// if bans have already been placed, beatmap changes result in a selection being made automatically
if (beatmap.NewValue?.OnlineID > 0)
addForBeatmap(beatmap.NewValue.OnlineID);
}

View File

@ -319,7 +319,7 @@ namespace osu.Game.Beatmaps
{
DateTimeOffset dateAdded = DateTimeOffset.UtcNow;
if (reader is LegacyDirectoryArchiveReader legacyReader)
if (reader is DirectoryArchiveReader legacyReader)
{
var beatmaps = reader.Filenames.Where(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));

View File

@ -26,6 +26,7 @@ using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Skinning;
using osu.Game.Utils;
using Realms;
namespace osu.Game.Beatmaps
{
@ -284,7 +285,7 @@ namespace osu.Game.Beatmaps
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r => r.All<BeatmapInfo>().FirstOrDefault(query)?.Detach());
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r => r.All<BeatmapInfo>().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach());
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.

View File

@ -23,11 +23,16 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary>
public readonly BindableDouble SliderVelocityBindable = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
};
/// <summary>
/// Whether or not slider ticks should be generated at this control point.
/// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991).
/// </summary>
public bool GenerateTicks { get; set; } = true;
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1;
/// <summary>
@ -41,11 +46,13 @@ namespace osu.Game.Beatmaps.ControlPoints
public override bool IsRedundant(ControlPoint? existing)
=> existing is DifficultyControlPoint existingDifficulty
&& GenerateTicks == existingDifficulty.GenerateTicks
&& SliderVelocity == existingDifficulty.SliderVelocity;
public override void CopyFrom(ControlPoint other)
{
SliderVelocity = ((DifficultyControlPoint)other).SliderVelocity;
GenerateTicks = ((DifficultyControlPoint)other).GenerateTicks;
base.CopyFrom(other);
}
@ -56,8 +63,10 @@ namespace osu.Game.Beatmaps.ControlPoints
public bool Equals(DifficultyControlPoint? other)
=> base.Equals(other)
&& GenerateTicks == other.GenerateTicks
&& SliderVelocity == other.SliderVelocity;
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity);
// ReSharper disable once NonReadonlyMemberInGetHashCode
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity, GenerateTicks);
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu
{
public const float TRANSITION_DURATION = 400;
public const float TRANSITION_DURATION = 340;
public const float CORNER_RADIUS = 10;
protected const float WIDTH = 430;
@ -89,6 +89,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
switch (size)
{
case BeatmapCardSize.Nano:
return new BeatmapCardNano(beatmapSet);
case BeatmapCardSize.Normal:
return new BeatmapCardNormal(beatmapSet, allowExpansion);

View File

@ -0,0 +1,169 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Beatmaps.Drawables.Cards
{
public partial class BeatmapCardNano : BeatmapCard
{
protected override Drawable IdleContent => idleBottomContent;
protected override Drawable DownloadInProgressContent => downloadProgressBar;
public override float Width
{
get => base.Width;
set
{
base.Width = value;
if (LoadState >= LoadState.Ready)
buttonContainer.Width = value;
}
}
private const float height = 60;
private const float width = 300;
[Cached]
private readonly BeatmapCardContent content;
private CollapsibleButtonContainer buttonContainer = null!;
private FillFlowContainer idleBottomContent = null!;
private BeatmapCardDownloadProgressBar downloadProgressBar = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public BeatmapCardNano(APIBeatmapSet beatmapSet)
: base(beatmapSet, false)
{
content = new BeatmapCardContent(height);
}
[BackgroundDependencyLoader]
private void load()
{
Width = width;
Height = height;
Child = content.With(c =>
{
c.MainContent = new Container
{
RelativeSizeAxes = Axes.X,
Height = height,
Children = new Drawable[]
{
buttonContainer = new CollapsibleButtonContainer(BeatmapSet)
{
Width = Width,
FavouriteState = { BindTarget = FavouriteState },
ButtonsCollapsedWidth = 5,
ButtonsExpandedWidth = 30,
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new TruncatingSpriteText
{
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 19, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
new TruncatingSpriteText
{
Text = createArtistText(),
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
}
},
new Container
{
Name = @"Bottom content",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Children = new Drawable[]
{
idleBottomContent = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 3),
AlwaysPresent = true,
Children = new Drawable[]
{
new LinkFlowContainer(s =>
{
s.Shadow = false;
s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold);
}).With(d =>
{
d.AutoSizeAxes = Axes.Both;
d.Margin = new MarginPadding { Top = 2 };
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
d.AddUserLink(BeatmapSet.Author);
}),
}
},
downloadProgressBar = new BeatmapCardDownloadProgressBar
{
RelativeSizeAxes = Axes.X,
Height = 6,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { BindTarget = DownloadTracker.State },
Progress = { BindTarget = DownloadTracker.Progress }
}
}
}
}
}
}
};
c.ExpandedContent = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10, Vertical = 13 },
Child = new BeatmapCardDifficultyList(BeatmapSet)
};
c.Expanded.BindTarget = Expanded;
});
}
private LocalisableString createArtistText()
{
var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist);
return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);
}
protected override void UpdateState()
{
base.UpdateState();
bool showDetails = IsHovered;
buttonContainer.ShowDetails.Value = showDetails;
}
}
}

View File

@ -8,6 +8,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
/// </summary>
public enum BeatmapCardSize
{
Nano,
Normal,
Extra
}

View File

@ -3,15 +3,15 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Framework.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.Drawables.Cards
{
@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
set => foreground.Padding = value;
}
private readonly UpdateableOnlineBeatmapSetCover cover;
private readonly Box background;
private readonly Container foreground;
private readonly PlayButton playButton;
private readonly CircularProgress progress;
@ -33,15 +33,22 @@ namespace osu.Game.Beatmaps.Drawables.Cards
protected override Container<Drawable> Content => content;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public BeatmapCardThumbnail(APIBeatmapSet beatmapSetInfo)
{
InternalChildren = new Drawable[]
{
cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List)
new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List)
{
RelativeSizeAxes = Axes.Both,
OnlineInfo = beatmapSetInfo
},
background = new Box
{
RelativeSizeAxes = Axes.Both
},
foreground = new Container
{
RelativeSizeAxes = Axes.Both,
@ -68,7 +75,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
private void load()
{
progress.Colour = colourProvider.Highlight1;
}
@ -89,7 +96,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
bool shouldDim = Dimmed.Value || playButton.Playing.Value;
playButton.FadeTo(shouldDim ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
cover.FadeColour(shouldDim ? OsuColour.Gray(0.2f) : Color4.White, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
background.FadeColour(colourProvider.Background6.Opacity(shouldDim ? 0.8f : 0f), BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
}
}
}

View File

@ -36,9 +36,10 @@ namespace osu.Game.Beatmaps
BeatmapSet = new BeatmapSetInfo(),
Difficulty = new BeatmapDifficulty
{
DrainRate = 0,
CircleSize = 0,
DrainRate = 0,
OverallDifficulty = 0,
ApproachRate = 0,
},
Ruleset = new DummyRuleset().RulesetInfo
}, audio)

View File

@ -1,8 +1,6 @@
// 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.
#pragma warning disable 618
using System;
using System.Collections.Generic;
using System.IO;
@ -103,15 +101,11 @@ namespace osu.Game.Beatmaps.Formats
{
DifficultyControlPoint difficultyControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.DifficultyPointAt(hitObject.StartTime) ?? DifficultyControlPoint.DEFAULT;
if (difficultyControlPoint is LegacyDifficultyControlPoint legacyDifficultyControlPoint)
{
hitObject.LegacyBpmMultiplier = legacyDifficultyControlPoint.BpmMultiplier;
if (hitObject is IHasGenerateTicks hasGenerateTicks)
hasGenerateTicks.GenerateTicks = legacyDifficultyControlPoint.GenerateTicks;
}
if (hitObject is IHasGenerateTicks hasGenerateTicks)
hasGenerateTicks.GenerateTicks = difficultyControlPoint.GenerateTicks;
if (hitObject is IHasSliderVelocity hasSliderVelocity)
hasSliderVelocity.SliderVelocity = difficultyControlPoint.SliderVelocity;
hasSliderVelocity.SliderVelocityMultiplier = difficultyControlPoint.SliderVelocity;
hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
}
@ -497,8 +491,9 @@ namespace osu.Game.Beatmaps.Formats
int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
addControlPoint(time, new LegacyDifficultyControlPoint(onlineRulesetID, beatLength)
addControlPoint(time, new DifficultyControlPoint
{
GenerateTicks = !double.IsNaN(beatLength),
SliderVelocity = speedMultiplier,
}, timingChange);

View File

@ -269,7 +269,7 @@ namespace osu.Game.Beatmaps.Formats
foreach (var hitObject in hitObjects)
{
if (hitObject is IHasSliderVelocity hasSliderVelocity)
yield return new DifficultyControlPoint { Time = hitObject.StartTime, SliderVelocity = hasSliderVelocity.SliderVelocity };
yield return new DifficultyControlPoint { Time = hitObject.StartTime, SliderVelocity = hasSliderVelocity.SliderVelocityMultiplier };
}
}

View File

@ -163,63 +163,6 @@ namespace osu.Game.Beatmaps.Formats
Mania,
}
[Obsolete("Do not use unless you're a legacy ruleset and 100% sure.")]
public class LegacyDifficultyControlPoint : DifficultyControlPoint, IEquatable<LegacyDifficultyControlPoint>
{
/// <summary>
/// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it.
/// DO NOT USE THIS UNLESS 100% SURE.
/// </summary>
public double BpmMultiplier { get; private set; }
/// <summary>
/// Whether or not slider ticks should be generated at this control point.
/// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991).
/// </summary>
public bool GenerateTicks { get; private set; } = true;
public LegacyDifficultyControlPoint(int rulesetId, double beatLength)
: this()
{
// Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?).
if (rulesetId == 1 || rulesetId == 3)
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1;
else
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 1000) / 100.0 : 1;
GenerateTicks = !double.IsNaN(beatLength);
}
public LegacyDifficultyControlPoint()
{
SliderVelocityBindable.Precision = double.Epsilon;
}
public override bool IsRedundant(ControlPoint? existing)
=> base.IsRedundant(existing)
&& GenerateTicks == ((existing as LegacyDifficultyControlPoint)?.GenerateTicks ?? true);
public override void CopyFrom(ControlPoint other)
{
base.CopyFrom(other);
BpmMultiplier = ((LegacyDifficultyControlPoint)other).BpmMultiplier;
GenerateTicks = ((LegacyDifficultyControlPoint)other).GenerateTicks;
}
public override bool Equals(ControlPoint? other)
=> other is LegacyDifficultyControlPoint otherLegacyDifficultyControlPoint
&& Equals(otherLegacyDifficultyControlPoint);
public bool Equals(LegacyDifficultyControlPoint? other)
=> base.Equals(other)
&& BpmMultiplier == other.BpmMultiplier
&& GenerateTicks == other.GenerateTicks;
// ReSharper disable twice NonReadonlyMemberInGetHashCode
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), BpmMultiplier, GenerateTicks);
}
internal class LegacySampleControlPoint : SampleControlPoint, IEquatable<LegacySampleControlPoint>
{
public int CustomSampleBank;

View File

@ -64,7 +64,13 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.Username, string.Empty);
SetDefault(OsuSetting.Token, string.Empty);
#pragma warning disable CS0618 // Type or member is obsolete
// this default set MUST remain despite the setting being deprecated, because `SetDefault()` calls are implicitly used to declare the type returned for the lookup.
// if this is removed, the setting will be interpreted as a string, and `Migrate()` will fail due to cast failure.
// can be removed 20240618
SetDefault(OsuSetting.AutomaticallyDownloadWhenSpectating, false);
#pragma warning restore CS0618 // Type or member is obsolete
SetDefault(OsuSetting.AutomaticallyDownloadMissingBeatmaps, false);
SetDefault(OsuSetting.SavePassword, false).ValueChanged += enabled =>
{
@ -215,6 +221,12 @@ namespace osu.Game.Configuration
// migrations can be added here using a condition like:
// if (combined < 20220103) { performMigration() }
if (combined < 20230918)
{
#pragma warning disable CS0618 // Type or member is obsolete
SetValue(OsuSetting.AutomaticallyDownloadMissingBeatmaps, Get<bool>(OsuSetting.AutomaticallyDownloadWhenSpectating)); // can be removed 20240618
#pragma warning restore CS0618 // Type or member is obsolete
}
}
public override TrackedSettings CreateTrackedSettings()
@ -383,13 +395,17 @@ namespace osu.Game.Configuration
EditorShowHitMarkers,
EditorAutoSeekOnPlacement,
DiscordRichPresence,
[Obsolete($"Use {nameof(AutomaticallyDownloadMissingBeatmaps)} instead.")] // can be removed 20240318
AutomaticallyDownloadWhenSpectating,
ShowOnlineExplicitContent,
LastProcessedMetadataId,
SafeAreaConsiderations,
ComboColourNormalisationAmount,
ProfileCoverExpanded,
EditorLimitedDistanceSnap,
ReplaySettingsOverlay
ReplaySettingsOverlay,
AutomaticallyDownloadMissingBeatmaps,
}
}

View File

@ -46,9 +46,29 @@ namespace osu.Game.Database
/// </summary>
public ArchiveReader GetReader()
{
return Stream != null
? getReaderFrom(Stream)
: getReaderFrom(Path);
if (Stream == null)
{
if (ZipUtils.IsZipArchive(Path))
return new ZipArchiveReader(File.Open(Path, FileMode.Open, FileAccess.Read, FileShare.Read), System.IO.Path.GetFileName(Path));
if (Directory.Exists(Path))
return new DirectoryArchiveReader(Path);
if (File.Exists(Path))
return new SingleFileArchiveReader(Path);
throw new InvalidFormatException($"{Path} is not a valid archive");
}
if (Stream is not MemoryStream memoryStream)
{
// Path used primarily in tests (converting `ManifestResourceStream`s to `MemoryStream`s).
memoryStream = new MemoryStream(Stream.ReadAllBytesToArray());
Stream.Dispose();
}
if (ZipUtils.IsZipArchive(memoryStream))
return new ZipArchiveReader(memoryStream, Path);
return new MemoryStreamArchiveReader(memoryStream, Path);
}
/// <summary>
@ -60,43 +80,6 @@ namespace osu.Game.Database
File.Delete(Path);
}
/// <summary>
/// Creates an <see cref="ArchiveReader"/> from a stream.
/// </summary>
/// <param name="stream">A seekable stream containing the archive content.</param>
/// <returns>A reader giving access to the archive's content.</returns>
private ArchiveReader getReaderFrom(Stream stream)
{
if (!(stream is MemoryStream memoryStream))
{
// This isn't used in any current path. May need to reconsider for performance reasons (ie. if we don't expect the incoming stream to be copied out).
memoryStream = new MemoryStream(stream.ReadAllBytesToArray());
stream.Dispose();
}
if (ZipUtils.IsZipArchive(memoryStream))
return new ZipArchiveReader(memoryStream, Path);
return new LegacyByteArrayReader(memoryStream.ToArray(), Path);
}
/// <summary>
/// Creates an <see cref="ArchiveReader"/> from a valid storage path.
/// </summary>
/// <param name="path">A file or folder path resolving the archive content.</param>
/// <returns>A reader giving access to the archive's content.</returns>
private ArchiveReader getReaderFrom(string path)
{
if (ZipUtils.IsZipArchive(path))
return new ZipArchiveReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read), System.IO.Path.GetFileName(path));
if (Directory.Exists(path))
return new LegacyDirectoryArchiveReader(path);
if (File.Exists(path))
return new LegacyFileArchiveReader(path);
throw new InvalidFormatException($"{path} is not a valid archive");
}
public override string ToString() => System.IO.Path.GetFileName(Path);
}
}

View File

@ -0,0 +1,103 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Configuration;
using osu.Game.IO.Archives;
using osu.Game.Localisation;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Notifications;
using osu.Game.Scoring;
using Realms;
namespace osu.Game.Database
{
public partial class MissingBeatmapNotification : SimpleNotification
{
[Resolved]
private BeatmapModelDownloader beatmapDownloader { get; set; } = null!;
[Resolved]
private ScoreManager scoreManager { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
private readonly ArchiveReader scoreArchive;
private readonly APIBeatmapSet beatmapSetInfo;
private readonly string beatmapHash;
private Bindable<bool> autoDownloadConfig = null!;
private Bindable<bool> noVideoSetting = null!;
private BeatmapCardNano card = null!;
private IDisposable? realmSubscription;
public MissingBeatmapNotification(APIBeatmap beatmap, ArchiveReader scoreArchive, string beatmapHash)
{
beatmapSetInfo = beatmap.BeatmapSet!;
this.beatmapHash = beatmapHash;
this.scoreArchive = scoreArchive;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
realmSubscription = realm.RegisterForNotifications(
realm => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending), beatmapsChanged);
autoDownloadConfig = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadMissingBeatmaps);
noVideoSetting = config.GetBindable<bool>(OsuSetting.PreferNoVideo);
Content.Add(card = new BeatmapCardNano(beatmapSetInfo));
}
protected override void LoadComplete()
{
base.LoadComplete();
if (autoDownloadConfig.Value)
{
Text = NotificationsStrings.DownloadingBeatmapForReplay;
beatmapDownloader.Download(beatmapSetInfo, noVideoSetting.Value);
}
else
{
bool missingSetMatchesExistingOnlineId = realm.Run(r => r.All<BeatmapSetInfo>().Any(s => !s.DeletePending && s.OnlineID == beatmapSetInfo.OnlineID));
Text = missingSetMatchesExistingOnlineId ? NotificationsStrings.MismatchingBeatmapForReplay : NotificationsStrings.MissingBeatmapForReplay;
}
}
protected override void Update()
{
base.Update();
card.Width = Content.DrawWidth;
}
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
{
if (changes?.InsertedIndices == null) return;
if (sender.Any(s => s.Beatmaps.Any(b => b.MD5Hash == beatmapHash)))
{
string name = scoreArchive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase));
var importTask = new ImportTask(scoreArchive.GetStream(name), name);
scoreManager.Import(new[] { importTask });
realmSubscription?.Dispose();
Close(false);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
realmSubscription?.Dispose();
}
}
}

View File

@ -17,6 +17,10 @@ namespace osu.Game.Graphics.UserInterface
{
public partial class ShearedButton : OsuClickableContainer
{
public const float HEIGHT = 50;
public const float CORNER_RADIUS = 7;
public const float BORDER_THICKNESS = 2;
public LocalisableString Text
{
get => text.Text;
@ -83,12 +87,10 @@ namespace osu.Game.Graphics.UserInterface
/// </param>
public ShearedButton(float? width = null)
{
Height = 50;
Height = HEIGHT;
Padding = new MarginPadding { Horizontal = shear * 50 };
const float corner_radius = 7;
Content.CornerRadius = corner_radius;
Content.CornerRadius = CORNER_RADIUS;
Content.Shear = new Vector2(shear, 0);
Content.Masking = true;
Content.Anchor = Content.Origin = Anchor.Centre;
@ -98,9 +100,9 @@ namespace osu.Game.Graphics.UserInterface
backgroundLayer = new Container
{
RelativeSizeAxes = Axes.Y,
CornerRadius = corner_radius,
CornerRadius = CORNER_RADIUS,
Masking = true,
BorderThickness = 2,
BorderThickness = BORDER_THICKNESS,
Children = new Drawable[]
{
background = new Box

View File

@ -9,11 +9,11 @@ namespace osu.Game.IO.Archives
/// <summary>
/// Allows reading a single file from the provided byte array.
/// </summary>
public class LegacyByteArrayReader : ArchiveReader
public class ByteArrayArchiveReader : ArchiveReader
{
private readonly byte[] content;
public LegacyByteArrayReader(byte[] content, string filename)
public ByteArrayArchiveReader(byte[] content, string filename)
: base(filename)
{
this.content = content;

View File

@ -8,13 +8,13 @@ using System.Linq;
namespace osu.Game.IO.Archives
{
/// <summary>
/// Reads an archive from a directory on disk.
/// Reads an archive directly from a directory on disk.
/// </summary>
public class LegacyDirectoryArchiveReader : ArchiveReader
public class DirectoryArchiveReader : ArchiveReader
{
private readonly string path;
public LegacyDirectoryArchiveReader(string path)
public DirectoryArchiveReader(string path)
: base(Path.GetFileName(path))
{
// re-get full path to standardise with Directory.GetFiles return values below.

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
namespace osu.Game.IO.Archives
{
/// <summary>
/// Allows reading a single file from the provided memory stream.
/// </summary>
public class MemoryStreamArchiveReader : ArchiveReader
{
private readonly MemoryStream stream;
public MemoryStreamArchiveReader(MemoryStream stream, string filename)
: base(filename)
{
this.stream = stream;
}
public override Stream GetStream(string name) => new MemoryStream(stream.ToArray(), 0, (int)stream.Length);
public override void Dispose()
{
}
public override IEnumerable<string> Filenames => new[] { Name };
}
}

View File

@ -7,14 +7,14 @@ using System.IO;
namespace osu.Game.IO.Archives
{
/// <summary>
/// Reads a file on disk as an archive.
/// Reads a single file on disk as an archive.
/// Note: In this case, the file is not an extractable archive, use <see cref="ZipArchiveReader"/> instead.
/// </summary>
public class LegacyFileArchiveReader : ArchiveReader
public class SingleFileArchiveReader : ArchiveReader
{
private readonly string path;
public LegacyFileArchiveReader(string path)
public SingleFileArchiveReader(string path)
: base(Path.GetFileName(path))
{
// re-get full path to standardise

View File

@ -1,19 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class DifficultyMultiplierDisplayStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.DifficultyMultiplierDisplay";
/// <summary>
/// "Difficulty Multiplier"
/// </summary>
public static LocalisableString DifficultyMultiplier => new TranslatableString(getKey(@"difficulty_multiplier"), @"Difficulty Multiplier");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -44,6 +44,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString TabToSearch => new TranslatableString(getKey(@"tab_to_search"), @"tab to search...");
/// <summary>
/// "Score Multiplier"
/// </summary>
public static LocalisableString ScoreMultiplier => new TranslatableString(getKey(@"score_multiplier"), @"Score Multiplier");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
}

View File

@ -93,6 +93,21 @@ Please try changing your audio device to a working setting.");
/// </summary>
public static LocalisableString YourNameWasMentioned(string username) => new TranslatableString(getKey(@"your_name_was_mentioned"), @"Your name was mentioned in chat by '{0}'. Click to find out why!", username);
/// <summary>
/// "You do not have the beatmap for this replay."
/// </summary>
public static LocalisableString MissingBeatmapForReplay => new TranslatableString(getKey(@"missing_beatmap_for_replay"), @"You do not have the beatmap for this replay.");
/// <summary>
/// "Downloading missing beatmap for this replay..."
/// </summary>
public static LocalisableString DownloadingBeatmapForReplay => new TranslatableString(getKey(@"downloading_beatmap_for_replay"), @"Downloading missing beatmap for this replay...");
/// <summary>
/// "Your local copy of the beatmap for this replay appears to be different than expected. You may need to update or re-download it."
/// </summary>
public static LocalisableString MismatchingBeatmapForReplay => new TranslatableString(getKey(@"mismatching_beatmap_for_replay"), @"Your local copy of the beatmap for this replay appears to be different than expected. You may need to update or re-download it.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -55,9 +55,9 @@ namespace osu.Game.Localisation
public static LocalisableString PreferNoVideo => new TranslatableString(getKey(@"prefer_no_video"), @"Prefer downloads without video");
/// <summary>
/// "Automatically download beatmaps when spectating"
/// "Automatically download missing beatmaps"
/// </summary>
public static LocalisableString AutomaticallyDownloadWhenSpectating => new TranslatableString(getKey(@"automatically_download_when_spectating"), @"Automatically download beatmaps when spectating");
public static LocalisableString AutomaticallyDownloadMissingBeatmaps => new TranslatableString(getKey(@"automatically_download_missing_beatmaps"), @"Automatically download missing beatmaps");
/// <summary>
/// "Show explicit content in search results"

View File

@ -35,8 +35,8 @@ namespace osu.Game.Online.API
for (int i = 0; i < itemCount; i++)
{
output[reader.ReadString()] =
PrimitiveObjectFormatter.Instance.Deserialize(ref reader, options);
output[reader.ReadString()!] =
PrimitiveObjectFormatter.Instance.Deserialize(ref reader, options)!;
}
return output;

View File

@ -28,7 +28,7 @@ namespace osu.Game.Online.Chat
// http[s]://<domain>.<tld>[:port][/path][?query][#fragment]
private static readonly Regex advanced_link_regex = new Regex(
// protocol
@"(?<link>[a-z]*?:\/\/" +
@"(?<link>(https?|osu(mp)?):\/\/" +
// domain + tld
@"(?<domain>(?:[a-z0-9]\.|[a-z0-9][a-z0-9-]*[a-z0-9]\.)*[a-z0-9-]*[a-z0-9]" +
// port (optional)

View File

@ -22,8 +22,12 @@ namespace osu.Game.Overlays.BeatmapListing
public BeatmapListingCardSizeTabControl()
{
AutoSizeAxes = Axes.Both;
Items = new[] { BeatmapCardSize.Normal, BeatmapCardSize.Extra };
}
protected override bool AddEnumEntriesAutomatically => false;
protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer
{
AutoSizeAxes = Axes.Both,

View File

@ -0,0 +1,188 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods;
using osuTK;
using System.Threading;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
namespace osu.Game.Overlays.Mods
{
/// <summary>
/// On the mod select overlay, this provides a local updating view of BPM, star rating and other
/// difficulty attributes so the user can have a better insight into what mods are changing.
/// </summary>
public partial class BeatmapAttributesDisplay : ModFooterInformationDisplay
{
private StarRatingDisplay starRatingDisplay = null!;
private BPMDisplay bpmDisplay = null!;
private VerticalAttributeDisplay circleSizeDisplay = null!;
private VerticalAttributeDisplay drainRateDisplay = null!;
private VerticalAttributeDisplay approachRateDisplay = null!;
private VerticalAttributeDisplay overallDifficultyDisplay = null!;
public Bindable<IBeatmapInfo?> BeatmapInfo { get; } = new Bindable<IBeatmapInfo?>();
[Resolved]
private Bindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
public BindableBool Collapsed { get; } = new BindableBool(true);
private ModSettingChangeTracker? modSettingChangeTracker;
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
private CancellationTokenSource? cancellationSource;
private IBindable<StarDifficulty?> starDifficulty = null!;
private const float transition_duration = 250;
[BackgroundDependencyLoader]
private void load()
{
const float shear = ShearedOverlayContainer.SHEAR;
LeftContent.AddRange(new Drawable[]
{
starRatingDisplay = new StarRatingDisplay(default, animated: true)
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Shear = new Vector2(-shear, 0),
},
bpmDisplay = new BPMDisplay
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Shear = new Vector2(-shear, 0),
AutoSizeAxes = Axes.Y,
Width = 75,
}
});
RightContent.Alpha = 0;
RightContent.AddRange(new Drawable[]
{
circleSizeDisplay = new VerticalAttributeDisplay("CS") { Shear = new Vector2(-shear, 0), },
drainRateDisplay = new VerticalAttributeDisplay("HP") { Shear = new Vector2(-shear, 0), },
approachRateDisplay = new VerticalAttributeDisplay("AR") { Shear = new Vector2(-shear, 0), },
overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { Shear = new Vector2(-shear, 0), },
});
}
protected override void LoadComplete()
{
base.LoadComplete();
mods.BindValueChanged(_ =>
{
modSettingChangeTracker?.Dispose();
modSettingChangeTracker = new ModSettingChangeTracker(mods.Value);
modSettingChangeTracker.SettingChanged += _ => updateValues();
updateValues();
}, true);
BeatmapInfo.BindValueChanged(_ => updateValues(), true);
Collapsed.BindValueChanged(_ =>
{
// Only start autosize animations on first collapse toggle. This avoids an ugly initial presentation.
startAnimating();
updateCollapsedState();
});
updateCollapsedState();
}
protected override bool OnHover(HoverEvent e)
{
startAnimating();
updateCollapsedState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateCollapsedState();
base.OnHoverLost(e);
}
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnClick(ClickEvent e) => true;
private void startAnimating()
{
Content.AutoSizeEasing = Easing.OutQuint;
Content.AutoSizeDuration = transition_duration;
}
private void updateCollapsedState()
{
RightContent.FadeTo(Collapsed.Value && !IsHovered ? 0 : 1, transition_duration, Easing.OutQuint);
}
private void updateValues() => Scheduler.AddOnce(() =>
{
if (BeatmapInfo.Value == null)
return;
cancellationSource?.Cancel();
starDifficulty = difficultyCache.GetBindableDifficulty(BeatmapInfo.Value, (cancellationSource = new CancellationTokenSource()).Token);
starDifficulty.BindValueChanged(s =>
{
starRatingDisplay.Current.Value = s.NewValue ?? default;
if (!starRatingDisplay.IsPresent)
starRatingDisplay.FinishTransforms(true);
});
double rate = 1;
foreach (var mod in mods.Value.OfType<IApplicableToRate>())
rate = mod.ApplyToRate(0, rate);
bpmDisplay.Current.Value = BeatmapInfo.Value.BPM * rate;
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty);
foreach (var mod in mods.Value.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(adjustedDifficulty);
circleSizeDisplay.Current.Value = adjustedDifficulty.CircleSize;
drainRateDisplay.Current.Value = adjustedDifficulty.DrainRate;
approachRateDisplay.Current.Value = adjustedDifficulty.ApproachRate;
overallDifficultyDisplay.Current.Value = adjustedDifficulty.OverallDifficulty;
});
private partial class BPMDisplay : RollingCounter<double>
{
protected override double RollingDuration => 500;
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0 BPM");
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Font = OsuFont.Default.With(size: 20, weight: FontWeight.SemiBold),
UseFullGlyphHeight = false,
};
}
}
}

View File

@ -1,41 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Mods
{
public sealed partial class DifficultyMultiplierDisplay : ModsEffectDisplay
{
protected override LocalisableString Label => DifficultyMultiplierDisplayStrings.DifficultyMultiplier;
protected override string CounterFormat => @"N2";
public DifficultyMultiplierDisplay()
{
Current.Default = 1d;
Current.Value = 1d;
Add(new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.Times,
Size = new Vector2(7),
Margin = new MarginPadding { Top = 1 }
});
}
protected override void LoadComplete()
{
base.LoadComplete();
// required to prevent the counter initially rolling up from 0 to 1
// due to `Current.Value` having a nonstandard default value of 1.
Counter.SetCountWithoutRolling(Current.Value);
}
}
}

View File

@ -0,0 +1,109 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Mods
{
public abstract partial class ModFooterInformationDisplay : CompositeDrawable
{
protected FillFlowContainer LeftContent { get; private set; } = null!;
protected FillFlowContainer RightContent { get; private set; } = null!;
protected Container Content { get; private set; } = null!;
private Container innerContent = null!;
protected Box MainBackground { get; private set; } = null!;
protected Box FrontBackground { get; private set; } = null!;
[Resolved]
protected OverlayColourProvider ColourProvider { get; private set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChild = Content = new Container
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
AutoSizeAxes = Axes.X,
Height = ShearedButton.HEIGHT,
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0),
CornerRadius = ShearedButton.CORNER_RADIUS,
BorderThickness = ShearedButton.BORDER_THICKNESS,
Masking = true,
Children = new Drawable[]
{
MainBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer // divide inner and outer content
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
innerContent = new Container
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
BorderThickness = ShearedButton.BORDER_THICKNESS,
CornerRadius = ShearedButton.CORNER_RADIUS,
Masking = true,
Children = new Drawable[]
{
FrontBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
LeftContent = new FillFlowContainer // actual inner content
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Margin = new MarginPadding { Horizontal = 15 },
Spacing = new Vector2(10),
}
}
},
RightContent = new FillFlowContainer
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
}
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
MainBackground.Colour = ColourProvider.Background4;
FrontBackground.Colour = ColourProvider.Background3;
Color4 glowColour = ColourProvider.Background1;
Content.BorderColour = ColourInfo.GradientVertical(MainBackground.Colour, glowColour);
innerContent.BorderColour = ColourInfo.GradientVertical(FrontBackground.Colour, glowColour);
}
}
}

View File

@ -26,6 +26,8 @@ namespace osu.Game.Overlays.Mods
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
private const float contracted_width = WIDTH - 120;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@ -42,6 +44,8 @@ namespace osu.Game.Overlays.Mods
base.LoadComplete();
ruleset.BindValueChanged(_ => rulesetChanged(), true);
Width = contracted_width;
}
private IDisposable? presetSubscription;
@ -65,7 +69,11 @@ namespace osu.Game.Overlays.Mods
{
cancellationTokenSource?.Cancel();
if (!presets.Any())
bool hasPresets = presets.Any();
this.ResizeWidthTo(hasPresets ? WIDTH : contracted_width, 200, Easing.OutQuint);
if (!hasPresets)
{
removeAndDisposePresetPanels();
return;

View File

@ -61,9 +61,11 @@ namespace osu.Game.Overlays.Mods
private const float header_height = 42;
protected const float WIDTH = 320;
protected ModSelectColumn()
{
Width = 320;
Width = WIDTH;
RelativeSizeAxes = Axes.Y;
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0);

View File

@ -17,6 +17,7 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@ -77,9 +78,9 @@ namespace osu.Game.Overlays.Mods
public ShearedSearchTextBox SearchTextBox { get; private set; } = null!;
/// <summary>
/// Whether the total score multiplier calculated from the current selected set of mods should be shown.
/// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown.
/// </summary>
protected virtual bool ShowTotalMultiplier => true;
protected virtual bool ShowModEffects => true;
/// <summary>
/// Whether per-mod customisation controls are visible.
@ -119,10 +120,12 @@ namespace osu.Game.Overlays.Mods
private ColumnScrollContainer columnScroll = null!;
private ColumnFlowContainer columnFlow = null!;
private FillFlowContainer<ShearedButton> footerButtonFlow = null!;
private FillFlowContainer footerContentFlow = null!;
private DeselectAllModsButton deselectAllModsButton = null!;
private Container aboveColumnsContent = null!;
private DifficultyMultiplierDisplay? multiplierDisplay;
private ScoreMultiplierDisplay? multiplierDisplay;
private BeatmapAttributesDisplay? beatmapAttributesDisplay;
protected ShearedButton BackButton { get; private set; } = null!;
protected ShearedToggleButton? CustomisationButton { get; private set; }
@ -130,6 +133,21 @@ namespace osu.Game.Overlays.Mods
private Sample? columnAppearSample;
private WorkingBeatmap? beatmap;
public WorkingBeatmap? Beatmap
{
get => beatmap;
set
{
if (beatmap == value) return;
beatmap = value;
if (IsLoaded && beatmapAttributesDisplay != null)
beatmapAttributesDisplay.BeatmapInfo.Value = beatmap?.BeatmapInfo;
}
}
protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
: base(colourScheme)
{
@ -164,7 +182,7 @@ namespace osu.Game.Overlays.Mods
aboveColumnsContent = new Container
{
RelativeSizeAxes = Axes.X,
Height = ModsEffectDisplay.HEIGHT,
Height = ScoreMultiplierDisplay.HEIGHT,
Padding = new MarginPadding { Horizontal = 100 },
Child = SearchTextBox = new ShearedSearchTextBox
{
@ -179,7 +197,7 @@ namespace osu.Game.Overlays.Mods
{
Padding = new MarginPadding
{
Top = ModsEffectDisplay.HEIGHT + PADDING,
Top = ScoreMultiplierDisplay.HEIGHT + PADDING,
Bottom = PADDING
},
RelativeSizeAxes = Axes.Both,
@ -210,16 +228,7 @@ namespace osu.Game.Overlays.Mods
}
});
if (ShowTotalMultiplier)
{
aboveColumnsContent.Add(multiplierDisplay = new DifficultyMultiplierDisplay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight
});
}
FooterContent.Child = footerButtonFlow = new FillFlowContainer<ShearedButton>
FooterContent.Add(footerButtonFlow = new FillFlowContainer<ShearedButton>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@ -239,7 +248,38 @@ namespace osu.Game.Overlays.Mods
DarkerColour = colours.Pink2,
LighterColour = colours.Pink1
})
};
});
if (ShowModEffects)
{
FooterContent.Add(footerContentFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(30, 10),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding
{
Vertical = PADDING,
Horizontal = 20
},
Children = new Drawable[]
{
multiplierDisplay = new ScoreMultiplierDisplay
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight
},
beatmapAttributesDisplay = new BeatmapAttributesDisplay
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
BeatmapInfo = { Value = beatmap?.BeatmapInfo }
},
}
});
}
globalAvailableMods.BindTo(game.AvailableMods);
}
@ -309,6 +349,25 @@ namespace osu.Game.Overlays.Mods
base.Update();
SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? Resources.Localisation.Web.CommonStrings.InputSearch : ModSelectOverlayStrings.TabToSearch;
if (beatmapAttributesDisplay != null)
{
float rightEdgeOfLastButton = footerButtonFlow.Last().ScreenSpaceDrawQuad.TopRight.X;
// this is cheating a bit; the 640 value is hardcoded based on how wide the expanded panel _generally_ is.
// due to the transition applied, the raw screenspace quad of the panel cannot be used, as it will trigger an ugly feedback cycle of expanding and collapsing.
float projectedLeftEdgeOfExpandedBeatmapAttributesDisplay = footerButtonFlow.ToScreenSpace(footerButtonFlow.DrawSize - new Vector2(640, 0)).X;
bool screenIsntWideEnough = rightEdgeOfLastButton > projectedLeftEdgeOfExpandedBeatmapAttributesDisplay;
// only update preview panel's collapsed state after we are fully visible, to ensure all the buttons are where we expect them to be.
if (Alpha == 1)
beatmapAttributesDisplay.Collapsed.Value = screenIsntWideEnough;
footerContentFlow.LayoutDuration = 200;
footerContentFlow.LayoutEasing = Easing.OutQuint;
footerContentFlow.Direction = screenIsntWideEnough ? FillDirection.Vertical : FillDirection.Horizontal;
}
}
/// <summary>
@ -886,6 +945,9 @@ namespace osu.Game.Overlays.Mods
OnClicked?.Invoke();
return true;
case HoverEvent:
return false;
case MouseEvent:
return true;
}

View File

@ -1,223 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods;
using osuTK;
namespace osu.Game.Overlays.Mods
{
/// <summary>
/// Base class for displays of mods effects.
/// </summary>
public abstract partial class ModsEffectDisplay : Container, IHasCurrentValue<double>
{
public const float HEIGHT = 42;
private const float transition_duration = 200;
private readonly Box contentBackground;
private readonly Box labelBackground;
private readonly FillFlowContainer content;
public Bindable<double> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly BindableWithCurrent<double> current = new BindableWithCurrent<double>();
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
/// <summary>
/// Text to display in the left area of the display.
/// </summary>
protected abstract LocalisableString Label { get; }
protected virtual float ValueAreaWidth => 56;
protected virtual string CounterFormat => @"N0";
protected override Container<Drawable> Content => content;
protected readonly RollingCounter<double> Counter;
protected ModsEffectDisplay()
{
Height = HEIGHT;
AutoSizeAxes = Axes.X;
InternalChild = new InputBlockingContainer
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Masking = true,
CornerRadius = ModSelectPanel.CORNER_RADIUS,
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0),
Children = new Drawable[]
{
contentBackground = new Box
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Y,
Width = ValueAreaWidth + ModSelectPanel.CORNER_RADIUS
},
new GridContainer
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, ValueAreaWidth)
},
Content = new[]
{
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Masking = true,
CornerRadius = ModSelectPanel.CORNER_RADIUS,
Children = new Drawable[]
{
labelBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Horizontal = 18 },
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Text = Label,
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
}
}
},
content = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Spacing = new Vector2(2, 0),
Child = Counter = new EffectCounter(CounterFormat)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = { BindTarget = Current }
}
}
}
}
}
}
};
}
[BackgroundDependencyLoader]
private void load()
{
labelBackground.Colour = colourProvider.Background4;
}
protected override void LoadComplete()
{
Current.BindValueChanged(e =>
{
var effect = CalculateEffectForComparison(e.NewValue.CompareTo(Current.Default));
setColours(effect);
}, true);
}
/// <summary>
/// Fades colours of text and its background according to displayed value.
/// </summary>
/// <param name="effect">Effect of the value.</param>
private void setColours(ModEffect effect)
{
switch (effect)
{
case ModEffect.NotChanged:
contentBackground.FadeColour(colourProvider.Background3, transition_duration, Easing.OutQuint);
content.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
break;
case ModEffect.DifficultyReduction:
contentBackground.FadeColour(colours.ForModType(ModType.DifficultyReduction), transition_duration, Easing.OutQuint);
content.FadeColour(colourProvider.Background5, transition_duration, Easing.OutQuint);
break;
case ModEffect.DifficultyIncrease:
contentBackground.FadeColour(colours.ForModType(ModType.DifficultyIncrease), transition_duration, Easing.OutQuint);
content.FadeColour(colourProvider.Background5, transition_duration, Easing.OutQuint);
break;
default:
throw new ArgumentOutOfRangeException(nameof(effect));
}
}
/// <summary>
/// Converts signed integer into <see cref="ModEffect"/>. Negative values are counted as difficulty reduction, positive as increase.
/// </summary>
/// <param name="comparison">Value to convert. Will arrive from comparison between <see cref="Current"/> bindable once it changes and it's <see cref="Bindable{T}.Default"/>.</param>
/// <returns>Effect of the value.</returns>
protected virtual ModEffect CalculateEffectForComparison(int comparison)
{
if (comparison == 0)
return ModEffect.NotChanged;
if (comparison < 0)
return ModEffect.DifficultyReduction;
return ModEffect.DifficultyIncrease;
}
protected enum ModEffect
{
NotChanged,
DifficultyReduction,
DifficultyIncrease
}
private partial class EffectCounter : RollingCounter<double>
{
private readonly string? format;
public EffectCounter(string? format)
{
this.format = format;
}
protected override double RollingDuration => 500;
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(format);
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
{
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
};
}
}
}

Some files were not shown because too many files have changed in this diff Show More