mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 17:35:10 +08:00
Merge branch 'master' into fix-slider-length
This commit is contained in:
commit
28da5baea4
@ -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" />
|
||||
|
@ -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" />
|
||||
|
@ -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" />
|
||||
|
@ -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" />
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
157
osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs
Normal file
157
osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs
Normal 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 };
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
175
osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs
Normal file
175
osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs
Normal 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 };
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
176
osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs
Normal file
176
osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs
Normal 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 };
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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,7 +68,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
switch (obj)
|
||||
{
|
||||
case DrawableSliderHead head:
|
||||
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
|
||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||
applyEarlyFading(head);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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) =>
|
||||
|
185
osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs
Normal file
185
osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs
Normal 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 };
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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()
|
||||
{
|
||||
|
19
osu.Game.Tests/Resources/invalid-bank.osu
Normal file
19
osu.Game.Tests/Resources/invalid-bank.osu
Normal 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:
|
@ -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()
|
||||
{
|
||||
|
@ -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();
|
||||
|
||||
|
@ -6,11 +6,14 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.PolygonExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
@ -139,6 +142,31 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
=> AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFriendScore()
|
||||
{
|
||||
APIUser friend = new APIUser { Username = "my friend", Id = 10000 };
|
||||
|
||||
createLeaderboard();
|
||||
addLocalPlayer();
|
||||
|
||||
AddStep("Add friend to API", () =>
|
||||
{
|
||||
var api = (DummyAPIAccess)API;
|
||||
|
||||
api.Friends.Clear();
|
||||
api.Friends.Add(friend);
|
||||
});
|
||||
|
||||
int playerNumber = 1;
|
||||
|
||||
AddRepeatStep("add 3 other players", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 3);
|
||||
AddUntilStep("there are no pink color score", () => leaderboard.ChildrenOfType<Box>().All(b => b.Colour != Color4Extensions.FromHex("ff549a")));
|
||||
|
||||
AddRepeatStep("add 3 friend score", () => createRandomScore(friend), 3);
|
||||
AddUntilStep("there are pink color for friend score", () => leaderboard.GetScoreByUsername("my friend").ChildrenOfType<Box>().Any(b => b.Colour == Color4Extensions.FromHex("ff549a")));
|
||||
}
|
||||
|
||||
private void addLocalPlayer()
|
||||
{
|
||||
AddStep("add local player", () =>
|
||||
@ -179,6 +207,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
return scoreItem != null && scoreItem.ScorePosition == expectedPosition;
|
||||
}
|
||||
|
||||
public GameplayLeaderboardScore GetScoreByUsername(string username)
|
||||
{
|
||||
return Flow.FirstOrDefault(i => i.User?.Username == username);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,636 +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.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
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 SettingsCheckbox scaleToMax = null!;
|
||||
|
||||
private FillFlowContainer<LegendEntry> legend = null!;
|
||||
|
||||
private readonly BindableBool standardisedVisible = new BindableBool(true);
|
||||
private readonly BindableBool classicVisible = new BindableBool(true);
|
||||
private readonly BindableBool scoreV1Visible = new BindableBool(true);
|
||||
private readonly BindableBool scoreV2Visible = new BindableBool(true);
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("setup tests", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Colour4.Black
|
||||
},
|
||||
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<LegendEntry>
|
||||
{
|
||||
Padding = new MarginPadding(20),
|
||||
Direction = FillDirection.Vertical,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding(20),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
sliderMaxCombo = new SettingsSlider<int>
|
||||
{
|
||||
TransferValueOnCommit = true,
|
||||
Current = new BindableInt(1024)
|
||||
{
|
||||
MinValue = 96,
|
||||
MaxValue = 8192,
|
||||
},
|
||||
LabelText = "Max combo",
|
||||
},
|
||||
scaleToMax = new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Rescale plots to 100%",
|
||||
Current = { Value = true, Default = true }
|
||||
},
|
||||
new OsuTextFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Text = $"Left click to add miss\nRight click to add OK/{base_ok}",
|
||||
Margin = new MarginPadding { Top = 20 }
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sliderMaxCombo.Current.BindValueChanged(_ => rerun());
|
||||
scaleToMax.Current.BindValueChanged(_ => rerun());
|
||||
|
||||
standardisedVisible.BindValueChanged(_ => rescalePlots());
|
||||
classicVisible.BindValueChanged(_ => rescalePlots());
|
||||
scoreV1Visible.BindValueChanged(_ => rescalePlots());
|
||||
scoreV2Visible.BindValueChanged(_ => rescalePlots());
|
||||
|
||||
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", colours.Green1, new OsuScoreProcessor(), ScoringMode.Standardised, standardisedVisible);
|
||||
runForProcessor("lazer-classic", colours.Blue1, new OsuScoreProcessor(), ScoringMode.Classic, classicVisible);
|
||||
|
||||
runScoreV1();
|
||||
runScoreV2();
|
||||
|
||||
rescalePlots();
|
||||
}
|
||||
|
||||
private void rescalePlots()
|
||||
{
|
||||
if (!scaleToMax.Current.Value && legend.Any(entry => entry.Visible.Value))
|
||||
{
|
||||
long maxScore = legend.Where(entry => entry.Visible.Value).Max(entry => entry.FinalScore);
|
||||
|
||||
foreach (var graph in graphs)
|
||||
graph.Height = graph.Values.Max() / maxScore;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var graph in graphs)
|
||||
graph.Height = 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void runScoreV1()
|
||||
{
|
||||
int totalScore = 0;
|
||||
int currentCombo = 0;
|
||||
|
||||
void applyHitV1(int baseScore)
|
||||
{
|
||||
if (baseScore == 0)
|
||||
{
|
||||
currentCombo = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// this corresponds to stable's `ScoreMultiplier`.
|
||||
// value is chosen arbitrarily, towards the upper range.
|
||||
const float score_multiplier = 4;
|
||||
|
||||
totalScore += baseScore;
|
||||
|
||||
// combo multiplier
|
||||
// ReSharper disable once PossibleLossOfFraction
|
||||
totalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier));
|
||||
|
||||
currentCombo++;
|
||||
}
|
||||
|
||||
runForAlgorithm(new ScoringAlgorithm
|
||||
{
|
||||
Name = "ScoreV1 (classic)",
|
||||
Colour = colours.Purple1,
|
||||
ApplyHit = () => applyHitV1(base_great),
|
||||
ApplyNonPerfect = () => applyHitV1(base_ok),
|
||||
ApplyMiss = () => applyHitV1(0),
|
||||
GetTotalScore = () => totalScore,
|
||||
Visible = scoreV1Visible
|
||||
});
|
||||
}
|
||||
|
||||
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(new ScoringAlgorithm
|
||||
{
|
||||
Name = "ScoreV2",
|
||||
Colour = colours.Red1,
|
||||
ApplyHit = () => applyHitV2(base_great),
|
||||
ApplyNonPerfect = () => applyHitV2(base_ok),
|
||||
ApplyMiss = () =>
|
||||
{
|
||||
currentHits++;
|
||||
maxBaseScore += base_great;
|
||||
currentCombo = 0;
|
||||
},
|
||||
GetTotalScore = () =>
|
||||
{
|
||||
double accuracy = currentBaseScore / maxBaseScore;
|
||||
|
||||
return (int)Math.Round
|
||||
(
|
||||
700000 * comboPortion / comboPortionMax +
|
||||
300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
|
||||
);
|
||||
},
|
||||
Visible = scoreV2Visible
|
||||
});
|
||||
}
|
||||
|
||||
private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode, BindableBool visibility)
|
||||
{
|
||||
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(new ScoringAlgorithm
|
||||
{
|
||||
Name = name,
|
||||
Colour = colour,
|
||||
ApplyHit = () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }),
|
||||
ApplyNonPerfect = () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
|
||||
ApplyMiss = () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }),
|
||||
GetTotalScore = () => processor.GetDisplayScore(mode),
|
||||
Visible = visibility
|
||||
});
|
||||
}
|
||||
|
||||
private void runForAlgorithm(ScoringAlgorithm scoringAlgorithm)
|
||||
{
|
||||
int maxCombo = sliderMaxCombo.Current.Value;
|
||||
|
||||
List<float> results = new List<float>();
|
||||
|
||||
for (int i = 0; i < maxCombo; i++)
|
||||
{
|
||||
if (graphs.MissLocations.Contains(i))
|
||||
scoringAlgorithm.ApplyMiss();
|
||||
else if (graphs.NonPerfectLocations.Contains(i))
|
||||
scoringAlgorithm.ApplyNonPerfect();
|
||||
else
|
||||
scoringAlgorithm.ApplyHit();
|
||||
|
||||
results.Add(scoringAlgorithm.GetTotalScore());
|
||||
}
|
||||
|
||||
LineGraph graph;
|
||||
graphs.Add(graph = new LineGraph
|
||||
{
|
||||
Name = scoringAlgorithm.Name,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
LineColour = scoringAlgorithm.Colour,
|
||||
Values = results
|
||||
});
|
||||
|
||||
legend.Add(new LegendEntry(scoringAlgorithm, graph)
|
||||
{
|
||||
AccentColour = scoringAlgorithm.Colour,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class ScoringAlgorithm
|
||||
{
|
||||
public string Name { get; init; } = null!;
|
||||
public Color4 Colour { get; init; }
|
||||
public Action ApplyHit { get; init; } = () => { };
|
||||
public Action ApplyNonPerfect { get; init; } = () => { };
|
||||
public Action ApplyMiss { get; init; } = () => { };
|
||||
public Func<long> GetTotalScore { get; init; } = null!;
|
||||
public BindableBool Visible { get; init; } = null!;
|
||||
}
|
||||
|
||||
public partial class GraphContainer : Container<LineGraph>, 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<LineGraph> Content { get; } = new Container<LineGraph> { 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;
|
||||
|
||||
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)
|
||||
{
|
||||
if (graph.Alpha == 0) continue;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class LegendEntry : OsuClickableContainer, IHasAccentColour
|
||||
{
|
||||
public Color4 AccentColour { get; set; }
|
||||
|
||||
public BindableBool Visible { get; } = new BindableBool(true);
|
||||
|
||||
public readonly long FinalScore;
|
||||
|
||||
private readonly string description;
|
||||
private readonly LineGraph lineGraph;
|
||||
|
||||
private OsuSpriteText descriptionText = null!;
|
||||
private OsuSpriteText finalScoreText = null!;
|
||||
|
||||
public LegendEntry(ScoringAlgorithm scoringAlgorithm, LineGraph lineGraph)
|
||||
{
|
||||
description = scoringAlgorithm.Name;
|
||||
FinalScore = scoringAlgorithm.GetTotalScore();
|
||||
AccentColour = scoringAlgorithm.Colour;
|
||||
Visible.BindTo(scoringAlgorithm.Visible);
|
||||
|
||||
this.lineGraph = lineGraph;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Content.RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Content.AutoSizeAxes = Axes.Y;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
descriptionText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
finalScoreText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Font = OsuFont.Default.With(fixedWidth: true)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Visible.BindValueChanged(_ => updateState(), true);
|
||||
Action = Visible.Toggle;
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateState();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
Colour = IsHovered ? AccentColour.Lighten(0.2f) : AccentColour;
|
||||
|
||||
descriptionText.Text = $"{(Visible.Value ? FontAwesome.Solid.CheckCircle.Icon : FontAwesome.Solid.Circle.Icon)} {description}";
|
||||
finalScoreText.Text = FinalScore.ToString("#,0");
|
||||
lineGraph.Alpha = Visible.Value ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@ -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")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
||||
|
169
osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs
Normal file
169
osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
/// </summary>
|
||||
public enum BeatmapCardSize
|
||||
{
|
||||
Nano,
|
||||
Normal,
|
||||
Extra
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
103
osu.Game/Database/MissingBeatmapNotification.cs
Normal file
103
osu.Game/Database/MissingBeatmapNotification.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ namespace osu.Game.IO.Archives
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
public override Stream GetStream(string name) => new MemoryStream(stream.GetBuffer(), 0, (int)stream.Length);
|
||||
public override Stream GetStream(string name) => new MemoryStream(stream.ToArray(), 0, (int)stream.Length);
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Overlays.Notifications
|
||||
|
||||
public bool WasClosed { get; private set; }
|
||||
|
||||
private readonly Container content;
|
||||
private readonly FillFlowContainer content;
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
@ -166,11 +166,13 @@ namespace osu.Game.Overlays.Notifications
|
||||
Padding = new MarginPadding(10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
content = new Container
|
||||
content = new FillFlowContainer
|
||||
{
|
||||
Masking = true,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(15)
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -31,9 +31,9 @@ namespace osu.Game.Overlays.Settings.Sections.Online
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = OnlineSettingsStrings.AutomaticallyDownloadWhenSpectating,
|
||||
Keywords = new[] { "spectator" },
|
||||
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadWhenSpectating),
|
||||
LabelText = OnlineSettingsStrings.AutomaticallyDownloadMissingBeatmaps,
|
||||
Keywords = new[] { "spectator", "replay" },
|
||||
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadMissingBeatmaps),
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
|
@ -190,7 +190,12 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
string[] split = str.Split(':');
|
||||
|
||||
var bank = (LegacySampleBank)Parsing.ParseInt(split[0]);
|
||||
if (!Enum.IsDefined(bank))
|
||||
bank = LegacySampleBank.Normal;
|
||||
|
||||
var addBank = (LegacySampleBank)Parsing.ParseInt(split[1]);
|
||||
if (!Enum.IsDefined(addBank))
|
||||
addBank = LegacySampleBank.Normal;
|
||||
|
||||
string stringBank = bank.ToString().ToLowerInvariant();
|
||||
if (stringBank == @"none")
|
||||
|
@ -54,7 +54,12 @@ namespace osu.Game.Scoring
|
||||
}
|
||||
catch (LegacyScoreDecoder.BeatmapNotFoundException e)
|
||||
{
|
||||
Logger.Log($@"Score '{name}' failed to import: no corresponding beatmap with the hash '{e.Hash}' could be found.", LoggingTarget.Database);
|
||||
Logger.Log($@"Score '{archive.Name}' failed to import: no corresponding beatmap with the hash '{e.Hash}' could be found.", LoggingTarget.Database);
|
||||
|
||||
// In the case of a missing beatmap, let's attempt to resolve it and show a prompt to the user to download the required beatmap.
|
||||
var req = new GetBeatmapRequest(new BeatmapInfo { MD5Hash = e.Hash });
|
||||
req.Success += res => PostNotification?.Invoke(new MissingBeatmapNotification(res, archive, e.Hash));
|
||||
api.Queue(req);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// 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.Framework.Extensions.Color4Extensions;
|
||||
@ -11,6 +12,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Users.Drawables;
|
||||
@ -107,6 +109,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private IBindable<ScoringMode> scoreDisplayMode = null!;
|
||||
|
||||
private bool isFriend;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="GameplayLeaderboardScore"/>.
|
||||
/// </summary>
|
||||
@ -124,7 +128,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, OsuConfigManager osuConfigManager)
|
||||
private void load(OsuColour colours, OsuConfigManager osuConfigManager, IAPIProvider api)
|
||||
{
|
||||
Container avatarContainer;
|
||||
|
||||
@ -311,6 +315,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
}, true);
|
||||
|
||||
HasQuit.BindValueChanged(_ => updateState());
|
||||
|
||||
isFriend = User != null && api.Friends.Any(u => User.OnlineID == u.Id);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -389,6 +395,11 @@ namespace osu.Game.Screens.Play.HUD
|
||||
panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966");
|
||||
textColour = TextColour ?? Color4Extensions.FromHex("2e576b");
|
||||
}
|
||||
else if (isFriend)
|
||||
{
|
||||
panelColour = BackgroundColour ?? Color4Extensions.FromHex("ff549a");
|
||||
textColour = TextColour ?? Color4.White;
|
||||
}
|
||||
else
|
||||
{
|
||||
panelColour = BackgroundColour ?? Color4Extensions.FromHex("3399cc");
|
||||
|
@ -143,7 +143,7 @@ namespace osu.Game.Screens.Play
|
||||
automaticDownload = new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Automatically download beatmaps",
|
||||
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadWhenSpectating),
|
||||
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadMissingBeatmaps),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
|
@ -1,22 +1,23 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osuTK;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Storyboards.Drawables
|
||||
{
|
||||
@ -57,12 +58,18 @@ namespace osu.Game.Storyboards.Drawables
|
||||
[Cached(typeof(IReadOnlyList<Mod>))]
|
||||
public IReadOnlyList<Mod> Mods { get; }
|
||||
|
||||
private DependencyContainer dependencies;
|
||||
[Resolved]
|
||||
private GameHost host { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
private DependencyContainer dependencies = null!;
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
|
||||
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
public DrawableStoryboard(Storyboard storyboard, IReadOnlyList<Mod> mods = null)
|
||||
public DrawableStoryboard(Storyboard storyboard, IReadOnlyList<Mod>? mods = null)
|
||||
{
|
||||
Storyboard = storyboard;
|
||||
Mods = mods ?? Array.Empty<Mod>();
|
||||
@ -85,12 +92,15 @@ namespace osu.Game.Storyboards.Drawables
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(IGameplayClock clock, CancellationToken? cancellationToken, GameHost host, RealmAccess realm)
|
||||
private void load(IGameplayClock? clock, CancellationToken? cancellationToken)
|
||||
{
|
||||
if (clock != null)
|
||||
Clock = clock;
|
||||
|
||||
dependencies.Cache(new TextureStore(host.Renderer, host.CreateTextureLoaderStore(new RealmFileStore(realm, host.Storage).Store), false, scaleAdjust: 1));
|
||||
dependencies.CacheAs(typeof(TextureStore),
|
||||
new TextureStore(host.Renderer, host.CreateTextureLoaderStore(
|
||||
CreateResourceLookupStore()
|
||||
), false, scaleAdjust: 1));
|
||||
|
||||
foreach (var layer in Storyboard.Layers)
|
||||
{
|
||||
@ -102,6 +112,8 @@ namespace osu.Game.Storyboards.Drawables
|
||||
lastEventEndTime = Storyboard.LatestEventTime;
|
||||
}
|
||||
|
||||
protected virtual IResourceStore<byte[]> CreateResourceLookupStore() => new StoryboardResourceLookupStore(Storyboard, realm, host);
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@ -115,5 +127,50 @@ namespace osu.Game.Storyboards.Drawables
|
||||
foreach (var layer in Children)
|
||||
layer.Enabled = passing ? layer.Layer.VisibleWhenPassing : layer.Layer.VisibleWhenFailing;
|
||||
}
|
||||
|
||||
private class StoryboardResourceLookupStore : IResourceStore<byte[]>
|
||||
{
|
||||
private readonly IResourceStore<byte[]> realmFileStore;
|
||||
private readonly Storyboard storyboard;
|
||||
|
||||
public StoryboardResourceLookupStore(Storyboard storyboard, RealmAccess realm, GameHost host)
|
||||
{
|
||||
realmFileStore = new RealmFileStore(realm, host.Storage).Store;
|
||||
this.storyboard = storyboard;
|
||||
}
|
||||
|
||||
public void Dispose() =>
|
||||
realmFileStore.Dispose();
|
||||
|
||||
public byte[] Get(string name)
|
||||
{
|
||||
string? storagePath = storyboard.GetStoragePathFromStoryboardPath(name);
|
||||
|
||||
return string.IsNullOrEmpty(storagePath)
|
||||
? null!
|
||||
: realmFileStore.Get(storagePath);
|
||||
}
|
||||
|
||||
public Task<byte[]> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
string? storagePath = storyboard.GetStoragePathFromStoryboardPath(name);
|
||||
|
||||
return string.IsNullOrEmpty(storagePath)
|
||||
? Task.FromResult<byte[]>(null!)
|
||||
: realmFileStore.GetAsync(storagePath, cancellationToken);
|
||||
}
|
||||
|
||||
public Stream? GetStream(string name)
|
||||
{
|
||||
string? storagePath = storyboard.GetStoragePathFromStoryboardPath(name);
|
||||
|
||||
return string.IsNullOrEmpty(storagePath)
|
||||
? null
|
||||
: realmFileStore.GetStream(storagePath);
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetAvailableResources() =>
|
||||
realmFileStore.GetAvailableResources();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,15 +99,13 @@ namespace osu.Game.Storyboards.Drawables
|
||||
{
|
||||
int frameIndex = 0;
|
||||
|
||||
Texture frameTexture = storyboard.GetTextureFromPath(getFramePath(frameIndex), textureStore);
|
||||
Texture frameTexture = textureStore.Get(getFramePath(frameIndex));
|
||||
|
||||
if (frameTexture != null)
|
||||
{
|
||||
// sourcing from storyboard.
|
||||
for (frameIndex = 0; frameIndex < Animation.FrameCount; frameIndex++)
|
||||
{
|
||||
AddFrame(storyboard.GetTextureFromPath(getFramePath(frameIndex), textureStore), Animation.FrameDelay);
|
||||
}
|
||||
AddFrame(textureStore.Get(getFramePath(frameIndex)), Animation.FrameDelay);
|
||||
}
|
||||
else if (storyboard.UseSkinSprites)
|
||||
{
|
||||
|
@ -90,7 +90,7 @@ namespace osu.Game.Storyboards.Drawables
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TextureStore textureStore, Storyboard storyboard)
|
||||
{
|
||||
Texture = storyboard.GetTextureFromPath(Sprite.Path, textureStore);
|
||||
Texture = textureStore.Get(Sprite.Path);
|
||||
|
||||
if (Texture == null && storyboard.UseSkinSprites)
|
||||
{
|
||||
|
@ -29,12 +29,7 @@ namespace osu.Game.Storyboards.Drawables
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(IBindable<WorkingBeatmap> beatmap, TextureStore textureStore)
|
||||
{
|
||||
string? path = beatmap.Value.BeatmapSetInfo?.GetPathForFile(Video.Path);
|
||||
|
||||
if (path == null)
|
||||
return;
|
||||
|
||||
var stream = textureStore.GetStream(path);
|
||||
var stream = textureStore.GetStream(Video.Path);
|
||||
|
||||
if (stream == null)
|
||||
return;
|
||||
|
@ -4,7 +4,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Storyboards.Drawables;
|
||||
@ -92,7 +91,7 @@ namespace osu.Game.Storyboards
|
||||
|
||||
private static readonly string[] image_extensions = { @".png", @".jpg" };
|
||||
|
||||
public Texture? GetTextureFromPath(string path, TextureStore textureStore)
|
||||
public virtual string? GetStoragePathFromStoryboardPath(string path)
|
||||
{
|
||||
string? resolvedPath = null;
|
||||
|
||||
@ -102,10 +101,7 @@ namespace osu.Game.Storyboards
|
||||
}
|
||||
else
|
||||
{
|
||||
// Just doing this extension logic locally here for simplicity.
|
||||
//
|
||||
// A more "sane" path may be to use the ISkinSource.GetTexture path (which will use the extensions of the underlying TextureStore),
|
||||
// but comes with potential complexity (what happens if the user has beatmap skins disabled?).
|
||||
// Some old storyboards don't include a file extension, so let's best guess at one.
|
||||
foreach (string ext in image_extensions)
|
||||
{
|
||||
if ((resolvedPath = BeatmapInfo.BeatmapSet?.GetPathForFile($"{path}{ext}")) != null)
|
||||
@ -113,10 +109,7 @@ namespace osu.Game.Storyboards
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(resolvedPath))
|
||||
return textureStore.Get(resolvedPath);
|
||||
|
||||
return null;
|
||||
return resolvedPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
596
osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs
Normal file
596
osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs
Normal file
@ -0,0 +1,596 @@
|
||||
// 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.Color4Extensions;
|
||||
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.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
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.Judgements;
|
||||
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 abstract partial class ScoringTestScene : OsuTestScene
|
||||
{
|
||||
protected abstract IBeatmap CreateBeatmap(int maxCombo);
|
||||
|
||||
protected abstract IScoringAlgorithm CreateScoreV1();
|
||||
protected abstract IScoringAlgorithm CreateScoreV2(int maxCombo);
|
||||
protected abstract ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode);
|
||||
|
||||
protected Bindable<int> MaxCombo => sliderMaxCombo.Current;
|
||||
protected BindableList<double> NonPerfectLocations => graphs.NonPerfectLocations;
|
||||
protected BindableList<double> MissLocations => graphs.MissLocations;
|
||||
|
||||
private readonly bool supportsNonPerfectJudgements;
|
||||
|
||||
private GraphContainer graphs = null!;
|
||||
private SettingsSlider<int> sliderMaxCombo = null!;
|
||||
private SettingsCheckbox scaleToMax = null!;
|
||||
|
||||
private FillFlowContainer<LegendEntry> legend = null!;
|
||||
|
||||
private readonly BindableBool standardisedVisible = new BindableBool(true);
|
||||
private readonly BindableBool classicVisible = new BindableBool(true);
|
||||
private readonly BindableBool scoreV1Visible = new BindableBool(true);
|
||||
private readonly BindableBool scoreV2Visible = new BindableBool(true);
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
protected ScoringTestScene(bool supportsNonPerfectJudgements = true)
|
||||
{
|
||||
this.supportsNonPerfectJudgements = supportsNonPerfectJudgements;
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("setup tests", () =>
|
||||
{
|
||||
OsuTextFlowContainer clickExplainer;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Colour4.Black
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
graphs = new GraphContainer(supportsNonPerfectJudgements)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
legend = new FillFlowContainer<LegendEntry>
|
||||
{
|
||||
Padding = new MarginPadding(20),
|
||||
Direction = FillDirection.Vertical,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding(20),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
sliderMaxCombo = new SettingsSlider<int>
|
||||
{
|
||||
TransferValueOnCommit = true,
|
||||
Current = new BindableInt(1024)
|
||||
{
|
||||
MinValue = 96,
|
||||
MaxValue = 8192,
|
||||
},
|
||||
LabelText = "Max combo",
|
||||
},
|
||||
scaleToMax = new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Rescale plots to 100%",
|
||||
Current = { Value = true, Default = true }
|
||||
},
|
||||
clickExplainer = new OsuTextFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Top = 20 }
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
clickExplainer.AddParagraph("Left click to add miss");
|
||||
if (supportsNonPerfectJudgements)
|
||||
clickExplainer.AddParagraph("Right click to add OK");
|
||||
|
||||
sliderMaxCombo.Current.BindValueChanged(_ => Rerun());
|
||||
scaleToMax.Current.BindValueChanged(_ => Rerun());
|
||||
|
||||
standardisedVisible.BindValueChanged(_ => rescalePlots());
|
||||
classicVisible.BindValueChanged(_ => rescalePlots());
|
||||
scoreV1Visible.BindValueChanged(_ => rescalePlots());
|
||||
scoreV2Visible.BindValueChanged(_ => rescalePlots());
|
||||
|
||||
graphs.MissLocations.BindCollectionChanged((_, __) => Rerun());
|
||||
graphs.NonPerfectLocations.BindCollectionChanged((_, __) => Rerun());
|
||||
|
||||
graphs.MaxCombo.BindTo(sliderMaxCombo.Current);
|
||||
|
||||
Rerun();
|
||||
});
|
||||
}
|
||||
|
||||
protected void Rerun()
|
||||
{
|
||||
graphs.Clear();
|
||||
legend.Clear();
|
||||
|
||||
runForProcessor("lazer-standardised", colours.Green1, ScoringMode.Standardised, standardisedVisible);
|
||||
runForProcessor("lazer-classic", colours.Blue1, ScoringMode.Classic, classicVisible);
|
||||
|
||||
runForAlgorithm(new ScoringAlgorithmInfo
|
||||
{
|
||||
Name = "ScoreV1 (classic)",
|
||||
Colour = colours.Purple1,
|
||||
Algorithm = CreateScoreV1(),
|
||||
Visible = scoreV1Visible
|
||||
});
|
||||
runForAlgorithm(new ScoringAlgorithmInfo
|
||||
{
|
||||
Name = "ScoreV2",
|
||||
Colour = colours.Red1,
|
||||
Algorithm = CreateScoreV2(sliderMaxCombo.Current.Value),
|
||||
Visible = scoreV2Visible
|
||||
});
|
||||
|
||||
rescalePlots();
|
||||
}
|
||||
|
||||
private void rescalePlots()
|
||||
{
|
||||
if (!scaleToMax.Current.Value && legend.Any(entry => entry.Visible.Value))
|
||||
{
|
||||
long maxScore = legend.Where(entry => entry.Visible.Value).Max(entry => entry.FinalScore);
|
||||
|
||||
foreach (var graph in graphs)
|
||||
graph.Height = graph.Values.Max() / maxScore;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var graph in graphs)
|
||||
graph.Height = 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void runForProcessor(string name, Color4 colour, ScoringMode scoringMode, BindableBool visibility)
|
||||
{
|
||||
int maxCombo = sliderMaxCombo.Current.Value;
|
||||
var beatmap = CreateBeatmap(maxCombo);
|
||||
var algorithm = CreateScoreAlgorithm(beatmap, scoringMode);
|
||||
|
||||
runForAlgorithm(new ScoringAlgorithmInfo
|
||||
{
|
||||
Name = name,
|
||||
Colour = colour,
|
||||
Algorithm = algorithm,
|
||||
Visible = visibility
|
||||
});
|
||||
}
|
||||
|
||||
private void runForAlgorithm(ScoringAlgorithmInfo algorithmInfo)
|
||||
{
|
||||
int maxCombo = sliderMaxCombo.Current.Value;
|
||||
|
||||
List<float> results = new List<float>();
|
||||
|
||||
for (int i = 0; i < maxCombo; i++)
|
||||
{
|
||||
if (graphs.MissLocations.Contains(i))
|
||||
algorithmInfo.Algorithm.ApplyMiss();
|
||||
else if (graphs.NonPerfectLocations.Contains(i))
|
||||
algorithmInfo.Algorithm.ApplyNonPerfect();
|
||||
else
|
||||
algorithmInfo.Algorithm.ApplyHit();
|
||||
|
||||
results.Add(algorithmInfo.Algorithm.TotalScore);
|
||||
}
|
||||
|
||||
LineGraph graph;
|
||||
graphs.Add(graph = new LineGraph
|
||||
{
|
||||
Name = algorithmInfo.Name,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
LineColour = algorithmInfo.Colour,
|
||||
Values = results
|
||||
});
|
||||
|
||||
legend.Add(new LegendEntry(algorithmInfo, graph)
|
||||
{
|
||||
AccentColour = algorithmInfo.Colour,
|
||||
});
|
||||
}
|
||||
|
||||
private class ScoringAlgorithmInfo
|
||||
{
|
||||
public string Name { get; init; } = null!;
|
||||
public Color4 Colour { get; init; }
|
||||
public IScoringAlgorithm Algorithm { get; init; } = null!;
|
||||
public BindableBool Visible { get; init; } = null!;
|
||||
}
|
||||
|
||||
protected interface IScoringAlgorithm
|
||||
{
|
||||
void ApplyHit();
|
||||
void ApplyNonPerfect();
|
||||
void ApplyMiss();
|
||||
|
||||
long TotalScore { get; }
|
||||
}
|
||||
|
||||
protected abstract class ProcessorBasedScoringAlgorithm : IScoringAlgorithm
|
||||
{
|
||||
protected abstract ScoreProcessor CreateScoreProcessor();
|
||||
protected abstract JudgementResult CreatePerfectJudgementResult();
|
||||
protected abstract JudgementResult CreateNonPerfectJudgementResult();
|
||||
protected abstract JudgementResult CreateMissJudgementResult();
|
||||
|
||||
private readonly ScoreProcessor scoreProcessor;
|
||||
private readonly ScoringMode mode;
|
||||
|
||||
protected ProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
|
||||
{
|
||||
this.mode = mode;
|
||||
scoreProcessor = CreateScoreProcessor();
|
||||
scoreProcessor.ApplyBeatmap(beatmap);
|
||||
}
|
||||
|
||||
public void ApplyHit() => scoreProcessor.ApplyResult(CreatePerfectJudgementResult());
|
||||
public void ApplyNonPerfect() => scoreProcessor.ApplyResult(CreateNonPerfectJudgementResult());
|
||||
public void ApplyMiss() => scoreProcessor.ApplyResult(CreateMissJudgementResult());
|
||||
|
||||
public long TotalScore => scoreProcessor.GetDisplayScore(mode);
|
||||
}
|
||||
|
||||
public partial class GraphContainer : Container<LineGraph>, IHasCustomTooltip<IEnumerable<LineGraph>>
|
||||
{
|
||||
private readonly bool supportsNonPerfectJudgements;
|
||||
|
||||
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<LineGraph> Content { get; } = new Container<LineGraph> { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private readonly Box hoverLine;
|
||||
|
||||
private readonly Container missLines;
|
||||
private readonly Container verticalGridLines;
|
||||
|
||||
public int CurrentHoverCombo { get; private set; }
|
||||
|
||||
public GraphContainer(bool supportsNonPerfectJudgements)
|
||||
{
|
||||
this.supportsNonPerfectJudgements = supportsNonPerfectJudgements;
|
||||
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 if (supportsNonPerfectJudgements)
|
||||
NonPerfectLocations.Add(CurrentHoverCombo);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private GraphTooltip? tooltip;
|
||||
|
||||
public ITooltip<IEnumerable<LineGraph>> GetCustomTooltip() => tooltip ??= new GraphTooltip(this);
|
||||
|
||||
public IEnumerable<LineGraph> TooltipContent => Content;
|
||||
|
||||
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)
|
||||
{
|
||||
if (graph.Alpha == 0) continue;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private partial class LegendEntry : OsuClickableContainer, IHasAccentColour
|
||||
{
|
||||
public Color4 AccentColour { get; set; }
|
||||
|
||||
public BindableBool Visible { get; } = new BindableBool(true);
|
||||
|
||||
public readonly long FinalScore;
|
||||
|
||||
private readonly string description;
|
||||
private readonly LineGraph lineGraph;
|
||||
|
||||
private OsuSpriteText descriptionText = null!;
|
||||
private OsuSpriteText finalScoreText = null!;
|
||||
|
||||
public LegendEntry(ScoringAlgorithmInfo scoringAlgorithmInfo, LineGraph lineGraph)
|
||||
{
|
||||
description = scoringAlgorithmInfo.Name;
|
||||
FinalScore = scoringAlgorithmInfo.Algorithm.TotalScore;
|
||||
AccentColour = scoringAlgorithmInfo.Colour;
|
||||
Visible.BindTo(scoringAlgorithmInfo.Visible);
|
||||
|
||||
this.lineGraph = lineGraph;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Content.RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Content.AutoSizeAxes = Axes.Y;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
descriptionText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
finalScoreText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Font = OsuFont.Default.With(fixedWidth: true)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Visible.BindValueChanged(_ => updateState(), true);
|
||||
Action = Visible.Toggle;
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateState();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
Colour = IsHovered ? AccentColour.Lighten(0.2f) : AccentColour;
|
||||
|
||||
descriptionText.Text = $"{(Visible.Value ? FontAwesome.Solid.CheckCircle.Icon : FontAwesome.Solid.Circle.Icon)} {description}";
|
||||
finalScoreText.Text = FinalScore.ToString("#,0");
|
||||
lineGraph.Alpha = Visible.Value ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -635,7 +635,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private T clone<T>(T incoming)
|
||||
{
|
||||
byte[]? serialized = MessagePackSerializer.Serialize(typeof(T), incoming, SignalRUnionWorkaroundResolver.OPTIONS);
|
||||
byte[] serialized = MessagePackSerializer.Serialize(typeof(T), incoming, SignalRUnionWorkaroundResolver.OPTIONS);
|
||||
return MessagePackSerializer.Deserialize<T>(serialized, SignalRUnionWorkaroundResolver.OPTIONS);
|
||||
}
|
||||
|
||||
|
@ -265,7 +265,7 @@ namespace osu.Game.Tests.Visual
|
||||
{
|
||||
Debug.Assert(original.BeatmapSet != null);
|
||||
|
||||
return new APIBeatmapSet
|
||||
var result = new APIBeatmapSet
|
||||
{
|
||||
OnlineID = original.BeatmapSet.OnlineID,
|
||||
Status = BeatmapOnlineStatus.Ranked,
|
||||
@ -301,6 +301,11 @@ namespace osu.Game.Tests.Visual
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var beatmap in result.Beatmaps)
|
||||
beatmap.BeatmapSet = result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected WorkingBeatmap CreateWorkingBeatmap(RulesetInfo ruleset) =>
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Tests.Visual
|
||||
{
|
||||
@ -79,6 +80,11 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
protected void LoadPlayer(Mod[] mods)
|
||||
{
|
||||
// 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();
|
||||
|
||||
var ruleset = CreatePlayerRuleset();
|
||||
Ruleset.Value = ruleset.RulesetInfo;
|
||||
|
||||
|
@ -21,13 +21,13 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||
<PackageReference Include="DiffPlex" Version="1.7.1" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.53" />
|
||||
<PackageReference Include="Humanizer" Version="2.14.1" />
|
||||
<PackageReference Include="MessagePack" Version="2.4.59" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="7.0.2" />
|
||||
<PackageReference Include="MessagePack" Version="2.5.124" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="7.0.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="7.0.11" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="7.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
@ -35,13 +35,13 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="11.1.2" />
|
||||
<PackageReference Include="Realm" Version="11.5.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.914.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.914.0" />
|
||||
<PackageReference Include="Sentry" Version="3.28.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||
<PackageReference Include="Sentry" Version="3.39.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.4" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.6" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||
|
||||
|
@ -34,9 +34,9 @@
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We don't really use the camera.</string>
|
||||
<string>We don't really use the camera.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>We don't really use the microphone.</string>
|
||||
<string>We don't really use the microphone.</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
@ -130,5 +130,7 @@
|
||||
<string>Editor</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.music-games</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
Loading…
Reference in New Issue
Block a user