mirror of
https://github.com/ppy/osu.git
synced 2025-01-26 17:53:53 +08:00
Merge branch 'master' into fix-slider-length
This commit is contained in:
commit
28da5baea4
@ -9,9 +9,9 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<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="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<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="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<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="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<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="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
<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="Clowd.Squirrel" Version="2.9.42" />
|
||||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||||
<PackageReference Include="System.IO.Packaging" Version="7.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>
|
||||||
<ItemGroup Label="Resources">
|
<ItemGroup Label="Resources">
|
||||||
<EmbeddedResource Include="lazer.ico" />
|
<EmbeddedResource Include="lazer.ico" />
|
||||||
|
@ -7,9 +7,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.4" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.13.8" />
|
||||||
<PackageReference Include="nunit" Version="3.13.3" />
|
<PackageReference Include="nunit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<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="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<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="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<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
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
@ -19,7 +20,6 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps.Legacy;
|
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
@ -35,16 +35,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
private int depthIndex;
|
private int depthIndex;
|
||||||
|
|
||||||
private readonly BindableBool snakingIn = new BindableBool();
|
private readonly BindableBool snakingIn = new BindableBool(true);
|
||||||
private readonly BindableBool snakingOut = new BindableBool();
|
private readonly BindableBool snakingOut = new BindableBool(true);
|
||||||
|
|
||||||
[SetUpSteps]
|
private float progressToHit;
|
||||||
public void SetUpSteps()
|
|
||||||
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
AddToggleStep("toggle snaking", v =>
|
base.LoadComplete();
|
||||||
|
|
||||||
|
AddToggleStep("disable snaking", v =>
|
||||||
{
|
{
|
||||||
snakingIn.Value = v;
|
snakingIn.Value = !v;
|
||||||
snakingOut.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);
|
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]
|
[Test]
|
||||||
public void TestVariousSliders()
|
public void TestVariousSliders()
|
||||||
{
|
{
|
||||||
@ -206,7 +226,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
StackHeight = 10
|
StackHeight = 10
|
||||||
};
|
};
|
||||||
|
|
||||||
return createDrawable(slider, 2, 2);
|
return createDrawable(slider, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats);
|
private Drawable testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats);
|
||||||
@ -229,6 +249,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
var slider = new Slider
|
var slider = new Slider
|
||||||
{
|
{
|
||||||
|
SliderVelocityMultiplier = speedMultiplier,
|
||||||
StartTime = Time.Current + time_offset,
|
StartTime = Time.Current + time_offset,
|
||||||
Position = new Vector2(0, -(distance / 2)),
|
Position = new Vector2(0, -(distance / 2)),
|
||||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||||
@ -240,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
StackHeight = stackHeight
|
StackHeight = stackHeight
|
||||||
};
|
};
|
||||||
|
|
||||||
return createDrawable(slider, circleSize, speedMultiplier);
|
return createDrawable(slider, circleSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable testPerfect(int repeats = 0)
|
private Drawable testPerfect(int repeats = 0)
|
||||||
@ -258,7 +279,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
RepeatCount = repeats,
|
RepeatCount = repeats,
|
||||||
};
|
};
|
||||||
|
|
||||||
return createDrawable(slider, 2, 3);
|
return createDrawable(slider, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable testLinear(int repeats = 0) => createLinear(repeats);
|
private Drawable testLinear(int repeats = 0) => createLinear(repeats);
|
||||||
@ -281,7 +302,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
RepeatCount = repeats,
|
RepeatCount = repeats,
|
||||||
};
|
};
|
||||||
|
|
||||||
return createDrawable(slider, 2, 3);
|
return createDrawable(slider, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable testBezier(int repeats = 0) => createBezier(repeats);
|
private Drawable testBezier(int repeats = 0) => createBezier(repeats);
|
||||||
@ -303,7 +324,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
RepeatCount = repeats,
|
RepeatCount = repeats,
|
||||||
};
|
};
|
||||||
|
|
||||||
return createDrawable(slider, 2, 3);
|
return createDrawable(slider, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable testLinearOverlapping(int repeats = 0) => createOverlapping(repeats);
|
private Drawable testLinearOverlapping(int repeats = 0) => createOverlapping(repeats);
|
||||||
@ -326,7 +347,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
RepeatCount = repeats,
|
RepeatCount = repeats,
|
||||||
};
|
};
|
||||||
|
|
||||||
return createDrawable(slider, 2, 3);
|
return createDrawable(slider, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable testCatmull(int repeats = 0) => createCatmull(repeats);
|
private Drawable testCatmull(int repeats = 0) => createCatmull(repeats);
|
||||||
@ -352,15 +373,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
NodeSamples = repeatSamples
|
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();
|
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty
|
||||||
cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier });
|
|
||||||
|
|
||||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty
|
|
||||||
{
|
{
|
||||||
CircleSize = circleSize,
|
CircleSize = circleSize,
|
||||||
SliderTickRate = 3
|
SliderTickRate = 3
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -33,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
public partial class TestSceneSliderSnaking : TestSceneOsuPlayer
|
public partial class TestSceneSliderSnaking : TestSceneOsuPlayer
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private AudioManager audioManager { get; set; }
|
private AudioManager audioManager { get; set; } = null!;
|
||||||
|
|
||||||
protected override bool Autoplay => autoplay;
|
protected override bool Autoplay => autoplay;
|
||||||
private bool autoplay;
|
private bool autoplay;
|
||||||
@ -41,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private readonly BindableBool snakingIn = new BindableBool();
|
private readonly BindableBool snakingIn = new BindableBool();
|
||||||
private readonly BindableBool snakingOut = 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 duration_of_span = 3605;
|
||||||
private const double fade_in_modifier = -1200;
|
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);
|
=> new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -57,15 +55,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
|
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Slider slider;
|
private Slider slider = null!;
|
||||||
private DrawableSlider drawableSlider;
|
private DrawableSlider? drawableSlider;
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public void Setup() => Schedule(() =>
|
|
||||||
{
|
|
||||||
slider = null;
|
|
||||||
drawableSlider = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
protected override bool HasCustomSteps => true;
|
protected override bool HasCustomSteps => true;
|
||||||
|
|
||||||
@ -135,9 +126,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRepeatArrowDoesNotMoveWhenHit()
|
public void TestRepeatArrowDoesNotMove([Values] bool useAutoplay)
|
||||||
{
|
{
|
||||||
AddStep("enable autoplay", () => autoplay = true);
|
AddStep($"set autoplay to {useAutoplay}", () => autoplay = useAutoplay);
|
||||||
setSnaking(true);
|
setSnaking(true);
|
||||||
CreateTest();
|
CreateTest();
|
||||||
// repeat might have a chance to update its position depending on where in the frame its hit,
|
// 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);
|
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)
|
private void retrieveSlider(int index)
|
||||||
{
|
{
|
||||||
AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
|
AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
|
||||||
addSeekStep(() => slider.StartTime);
|
addSeekStep(() => slider.StartTime);
|
||||||
AddUntilStep("retrieve drawable slider", () =>
|
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);
|
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<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 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 getSliderStart() => getSliderCurve().First();
|
||||||
private Vector2 getSliderEnd() => getSliderCurve().Last();
|
private Vector2 getSliderEnd() => getSliderCurve().Last();
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<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="Moq" Version="4.18.4" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<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.")]
|
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
|
||||||
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);
|
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.")]
|
[SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")]
|
||||||
public Bindable<bool> ClassicNoteLock { get; } = new BindableBool(true);
|
public Bindable<bool> ClassicNoteLock { get; } = new BindableBool(true);
|
||||||
|
|
||||||
@ -71,7 +68,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
switch (obj)
|
switch (obj)
|
||||||
{
|
{
|
||||||
case DrawableSliderHead head:
|
case DrawableSliderHead head:
|
||||||
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
|
|
||||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||||
applyEarlyFading(head);
|
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);
|
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
|
||||||
|
|
||||||
Ball.UpdateProgress(completionProgress);
|
Ball.UpdateProgress(completionProgress);
|
||||||
SliderBody?.UpdateProgress(completionProgress);
|
SliderBody?.UpdateProgress(HeadCircle.IsHit ? completionProgress : 0);
|
||||||
|
|
||||||
foreach (DrawableHitObject hitObject in NestedHitObjects)
|
foreach (DrawableHitObject hitObject in NestedHitObjects)
|
||||||
{
|
{
|
||||||
@ -317,7 +317,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
switch (state)
|
switch (state)
|
||||||
{
|
{
|
||||||
case ArmedState.Hit:
|
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.
|
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,9 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
@ -24,12 +22,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
|
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>();
|
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
||||||
|
|
||||||
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;
|
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;
|
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)
|
protected override HitResult ResultFor(double timeOffset)
|
||||||
{
|
{
|
||||||
Debug.Assert(HitObject != null);
|
Debug.Assert(HitObject != null);
|
||||||
|
@ -46,22 +46,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
|
|
||||||
BorderSize = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1;
|
BorderSize = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1;
|
||||||
BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
|
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) =>
|
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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<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="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<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]
|
[Test]
|
||||||
public void TestFallbackDecoderForCorruptedHeader()
|
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));
|
AddStep($"set {scheme} scheme", () => Child = createContent(scheme, creationFunc));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNano()
|
||||||
|
{
|
||||||
|
createTestCase(beatmapSetInfo => new BeatmapCardNano(beatmapSetInfo));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestNormal()
|
public void TestNormal()
|
||||||
{
|
{
|
||||||
|
@ -67,6 +67,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
private Player loadPlayerFor(RulesetInfo rulesetInfo)
|
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;
|
Ruleset.Value = rulesetInfo;
|
||||||
var ruleset = rulesetInfo.CreateInstance();
|
var ruleset = rulesetInfo.CreateInstance();
|
||||||
|
|
||||||
|
@ -6,11 +6,14 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Extensions.PolygonExtensions;
|
using osu.Framework.Extensions.PolygonExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osuTK;
|
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);
|
=> 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()
|
private void addLocalPlayer()
|
||||||
{
|
{
|
||||||
AddStep("add local player", () =>
|
AddStep("add local player", () =>
|
||||||
@ -179,6 +207,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
return scoreItem != null && scoreItem.ScorePosition == expectedPosition;
|
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" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="DeepEqual" Version="4.2.1" />
|
<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="Nito.AsyncEx" Version="5.1.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<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" />
|
<PackageReference Include="Moq" Version="4.18.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<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="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<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)
|
if (CurrentMatch.Value == null || CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2)
|
||||||
return;
|
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)
|
if (beatmap.NewValue?.OnlineID > 0)
|
||||||
addForBeatmap(beatmap.NewValue.OnlineID);
|
addForBeatmap(beatmap.NewValue.OnlineID);
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ using osu.Game.Overlays.Notifications;
|
|||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps
|
namespace osu.Game.Beatmaps
|
||||||
{
|
{
|
||||||
@ -284,7 +285,7 @@ namespace osu.Game.Beatmaps
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">The query.</param>
|
/// <param name="query">The query.</param>
|
||||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
/// <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>
|
/// <summary>
|
||||||
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
|
/// 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 abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu
|
||||||
{
|
{
|
||||||
public const float TRANSITION_DURATION = 400;
|
public const float TRANSITION_DURATION = 340;
|
||||||
public const float CORNER_RADIUS = 10;
|
public const float CORNER_RADIUS = 10;
|
||||||
|
|
||||||
protected const float WIDTH = 430;
|
protected const float WIDTH = 430;
|
||||||
@ -89,6 +89,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
|||||||
{
|
{
|
||||||
switch (size)
|
switch (size)
|
||||||
{
|
{
|
||||||
|
case BeatmapCardSize.Nano:
|
||||||
|
return new BeatmapCardNano(beatmapSet);
|
||||||
|
|
||||||
case BeatmapCardSize.Normal:
|
case BeatmapCardSize.Normal:
|
||||||
return new BeatmapCardNormal(beatmapSet, allowExpansion);
|
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>
|
/// </summary>
|
||||||
public enum BeatmapCardSize
|
public enum BeatmapCardSize
|
||||||
{
|
{
|
||||||
|
Nano,
|
||||||
Normal,
|
Normal,
|
||||||
Extra
|
Extra
|
||||||
}
|
}
|
||||||
|
@ -3,15 +3,15 @@
|
|||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Beatmaps.Drawables.Cards.Buttons;
|
using osu.Game.Beatmaps.Drawables.Cards.Buttons;
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps.Drawables.Cards
|
namespace osu.Game.Beatmaps.Drawables.Cards
|
||||||
{
|
{
|
||||||
@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
|||||||
set => foreground.Padding = value;
|
set => foreground.Padding = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly UpdateableOnlineBeatmapSetCover cover;
|
private readonly Box background;
|
||||||
private readonly Container foreground;
|
private readonly Container foreground;
|
||||||
private readonly PlayButton playButton;
|
private readonly PlayButton playButton;
|
||||||
private readonly CircularProgress progress;
|
private readonly CircularProgress progress;
|
||||||
@ -33,15 +33,22 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
|||||||
|
|
||||||
protected override Container<Drawable> Content => content;
|
protected override Container<Drawable> Content => content;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||||
|
|
||||||
public BeatmapCardThumbnail(APIBeatmapSet beatmapSetInfo)
|
public BeatmapCardThumbnail(APIBeatmapSet beatmapSetInfo)
|
||||||
{
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List)
|
new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
OnlineInfo = beatmapSetInfo
|
OnlineInfo = beatmapSetInfo
|
||||||
},
|
},
|
||||||
|
background = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
},
|
||||||
foreground = new Container
|
foreground = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
@ -68,7 +75,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OverlayColourProvider colourProvider)
|
private void load()
|
||||||
{
|
{
|
||||||
progress.Colour = colourProvider.Highlight1;
|
progress.Colour = colourProvider.Highlight1;
|
||||||
}
|
}
|
||||||
@ -89,7 +96,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
|||||||
bool shouldDim = Dimmed.Value || playButton.Playing.Value;
|
bool shouldDim = Dimmed.Value || playButton.Playing.Value;
|
||||||
|
|
||||||
playButton.FadeTo(shouldDim ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
|
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.Username, string.Empty);
|
||||||
SetDefault(OsuSetting.Token, 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);
|
SetDefault(OsuSetting.AutomaticallyDownloadWhenSpectating, false);
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
SetDefault(OsuSetting.AutomaticallyDownloadMissingBeatmaps, false);
|
||||||
|
|
||||||
SetDefault(OsuSetting.SavePassword, false).ValueChanged += enabled =>
|
SetDefault(OsuSetting.SavePassword, false).ValueChanged += enabled =>
|
||||||
{
|
{
|
||||||
@ -215,6 +221,12 @@ namespace osu.Game.Configuration
|
|||||||
|
|
||||||
// migrations can be added here using a condition like:
|
// migrations can be added here using a condition like:
|
||||||
// if (combined < 20220103) { performMigration() }
|
// 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()
|
public override TrackedSettings CreateTrackedSettings()
|
||||||
@ -383,13 +395,17 @@ namespace osu.Game.Configuration
|
|||||||
EditorShowHitMarkers,
|
EditorShowHitMarkers,
|
||||||
EditorAutoSeekOnPlacement,
|
EditorAutoSeekOnPlacement,
|
||||||
DiscordRichPresence,
|
DiscordRichPresence,
|
||||||
|
|
||||||
|
[Obsolete($"Use {nameof(AutomaticallyDownloadMissingBeatmaps)} instead.")] // can be removed 20240318
|
||||||
AutomaticallyDownloadWhenSpectating,
|
AutomaticallyDownloadWhenSpectating,
|
||||||
|
|
||||||
ShowOnlineExplicitContent,
|
ShowOnlineExplicitContent,
|
||||||
LastProcessedMetadataId,
|
LastProcessedMetadataId,
|
||||||
SafeAreaConsiderations,
|
SafeAreaConsiderations,
|
||||||
ComboColourNormalisationAmount,
|
ComboColourNormalisationAmount,
|
||||||
ProfileCoverExpanded,
|
ProfileCoverExpanded,
|
||||||
EditorLimitedDistanceSnap,
|
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;
|
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()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
|
@ -93,6 +93,21 @@ Please try changing your audio device to a working setting.");
|
|||||||
/// </summary>
|
/// </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);
|
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}";
|
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");
|
public static LocalisableString PreferNoVideo => new TranslatableString(getKey(@"prefer_no_video"), @"Prefer downloads without video");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Automatically download beatmaps when spectating"
|
/// "Automatically download missing beatmaps"
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// "Show explicit content in search results"
|
/// "Show explicit content in search results"
|
||||||
|
@ -35,8 +35,8 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
for (int i = 0; i < itemCount; i++)
|
for (int i = 0; i < itemCount; i++)
|
||||||
{
|
{
|
||||||
output[reader.ReadString()] =
|
output[reader.ReadString()!] =
|
||||||
PrimitiveObjectFormatter.Instance.Deserialize(ref reader, options);
|
PrimitiveObjectFormatter.Instance.Deserialize(ref reader, options)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
|
@ -22,8 +22,12 @@ namespace osu.Game.Overlays.BeatmapListing
|
|||||||
public BeatmapListingCardSizeTabControl()
|
public BeatmapListingCardSizeTabControl()
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
Items = new[] { BeatmapCardSize.Normal, BeatmapCardSize.Extra };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool AddEnumEntriesAutomatically => false;
|
||||||
|
|
||||||
protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer
|
protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both,
|
AutoSizeAxes = Axes.Both,
|
||||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Overlays.Notifications
|
|||||||
|
|
||||||
public bool WasClosed { get; private set; }
|
public bool WasClosed { get; private set; }
|
||||||
|
|
||||||
private readonly Container content;
|
private readonly FillFlowContainer content;
|
||||||
|
|
||||||
protected override Container<Drawable> Content => content;
|
protected override Container<Drawable> Content => content;
|
||||||
|
|
||||||
@ -166,11 +166,13 @@ namespace osu.Game.Overlays.Notifications
|
|||||||
Padding = new MarginPadding(10),
|
Padding = new MarginPadding(10),
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
content = new Container
|
content = new FillFlowContainer
|
||||||
{
|
{
|
||||||
Masking = true,
|
Masking = true,
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Spacing = new Vector2(15)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -31,9 +31,9 @@ namespace osu.Game.Overlays.Settings.Sections.Online
|
|||||||
},
|
},
|
||||||
new SettingsCheckbox
|
new SettingsCheckbox
|
||||||
{
|
{
|
||||||
LabelText = OnlineSettingsStrings.AutomaticallyDownloadWhenSpectating,
|
LabelText = OnlineSettingsStrings.AutomaticallyDownloadMissingBeatmaps,
|
||||||
Keywords = new[] { "spectator" },
|
Keywords = new[] { "spectator", "replay" },
|
||||||
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadWhenSpectating),
|
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadMissingBeatmaps),
|
||||||
},
|
},
|
||||||
new SettingsCheckbox
|
new SettingsCheckbox
|
||||||
{
|
{
|
||||||
|
@ -190,7 +190,12 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
|||||||
string[] split = str.Split(':');
|
string[] split = str.Split(':');
|
||||||
|
|
||||||
var bank = (LegacySampleBank)Parsing.ParseInt(split[0]);
|
var bank = (LegacySampleBank)Parsing.ParseInt(split[0]);
|
||||||
|
if (!Enum.IsDefined(bank))
|
||||||
|
bank = LegacySampleBank.Normal;
|
||||||
|
|
||||||
var addBank = (LegacySampleBank)Parsing.ParseInt(split[1]);
|
var addBank = (LegacySampleBank)Parsing.ParseInt(split[1]);
|
||||||
|
if (!Enum.IsDefined(addBank))
|
||||||
|
addBank = LegacySampleBank.Normal;
|
||||||
|
|
||||||
string stringBank = bank.ToString().ToLowerInvariant();
|
string stringBank = bank.ToString().ToLowerInvariant();
|
||||||
if (stringBank == @"none")
|
if (stringBank == @"none")
|
||||||
|
@ -54,7 +54,12 @@ namespace osu.Game.Scoring
|
|||||||
}
|
}
|
||||||
catch (LegacyScoreDecoder.BeatmapNotFoundException e)
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
@ -11,6 +12,7 @@ using osu.Framework.Graphics.Shapes;
|
|||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
using osu.Game.Users.Drawables;
|
using osu.Game.Users.Drawables;
|
||||||
@ -107,6 +109,8 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
|
|
||||||
private IBindable<ScoringMode> scoreDisplayMode = null!;
|
private IBindable<ScoringMode> scoreDisplayMode = null!;
|
||||||
|
|
||||||
|
private bool isFriend;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="GameplayLeaderboardScore"/>.
|
/// Creates a new <see cref="GameplayLeaderboardScore"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -124,7 +128,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours, OsuConfigManager osuConfigManager)
|
private void load(OsuColour colours, OsuConfigManager osuConfigManager, IAPIProvider api)
|
||||||
{
|
{
|
||||||
Container avatarContainer;
|
Container avatarContainer;
|
||||||
|
|
||||||
@ -311,6 +315,8 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
HasQuit.BindValueChanged(_ => updateState());
|
HasQuit.BindValueChanged(_ => updateState());
|
||||||
|
|
||||||
|
isFriend = User != null && api.Friends.Any(u => User.OnlineID == u.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -389,6 +395,11 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966");
|
panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966");
|
||||||
textColour = TextColour ?? Color4Extensions.FromHex("2e576b");
|
textColour = TextColour ?? Color4Extensions.FromHex("2e576b");
|
||||||
}
|
}
|
||||||
|
else if (isFriend)
|
||||||
|
{
|
||||||
|
panelColour = BackgroundColour ?? Color4Extensions.FromHex("ff549a");
|
||||||
|
textColour = TextColour ?? Color4.White;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
panelColour = BackgroundColour ?? Color4Extensions.FromHex("3399cc");
|
panelColour = BackgroundColour ?? Color4Extensions.FromHex("3399cc");
|
||||||
|
@ -143,7 +143,7 @@ namespace osu.Game.Screens.Play
|
|||||||
automaticDownload = new SettingsCheckbox
|
automaticDownload = new SettingsCheckbox
|
||||||
{
|
{
|
||||||
LabelText = "Automatically download beatmaps",
|
LabelText = "Automatically download beatmaps",
|
||||||
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadWhenSpectating),
|
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadMissingBeatmaps),
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
},
|
},
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using osuTK;
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.IO.Stores;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Storyboards.Drawables
|
namespace osu.Game.Storyboards.Drawables
|
||||||
{
|
{
|
||||||
@ -57,12 +58,18 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
[Cached(typeof(IReadOnlyList<Mod>))]
|
[Cached(typeof(IReadOnlyList<Mod>))]
|
||||||
public IReadOnlyList<Mod> Mods { get; }
|
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) =>
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
|
||||||
dependencies = new DependencyContainer(base.CreateChildDependencies(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;
|
Storyboard = storyboard;
|
||||||
Mods = mods ?? Array.Empty<Mod>();
|
Mods = mods ?? Array.Empty<Mod>();
|
||||||
@ -85,12 +92,15 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(IGameplayClock clock, CancellationToken? cancellationToken, GameHost host, RealmAccess realm)
|
private void load(IGameplayClock? clock, CancellationToken? cancellationToken)
|
||||||
{
|
{
|
||||||
if (clock != null)
|
if (clock != null)
|
||||||
Clock = clock;
|
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)
|
foreach (var layer in Storyboard.Layers)
|
||||||
{
|
{
|
||||||
@ -102,6 +112,8 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
lastEventEndTime = Storyboard.LatestEventTime;
|
lastEventEndTime = Storyboard.LatestEventTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual IResourceStore<byte[]> CreateResourceLookupStore() => new StoryboardResourceLookupStore(Storyboard, realm, host);
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
@ -115,5 +127,50 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
foreach (var layer in Children)
|
foreach (var layer in Children)
|
||||||
layer.Enabled = passing ? layer.Layer.VisibleWhenPassing : layer.Layer.VisibleWhenFailing;
|
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;
|
int frameIndex = 0;
|
||||||
|
|
||||||
Texture frameTexture = storyboard.GetTextureFromPath(getFramePath(frameIndex), textureStore);
|
Texture frameTexture = textureStore.Get(getFramePath(frameIndex));
|
||||||
|
|
||||||
if (frameTexture != null)
|
if (frameTexture != null)
|
||||||
{
|
{
|
||||||
// sourcing from storyboard.
|
// sourcing from storyboard.
|
||||||
for (frameIndex = 0; frameIndex < Animation.FrameCount; frameIndex++)
|
for (frameIndex = 0; frameIndex < Animation.FrameCount; frameIndex++)
|
||||||
{
|
AddFrame(textureStore.Get(getFramePath(frameIndex)), Animation.FrameDelay);
|
||||||
AddFrame(storyboard.GetTextureFromPath(getFramePath(frameIndex), textureStore), Animation.FrameDelay);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (storyboard.UseSkinSprites)
|
else if (storyboard.UseSkinSprites)
|
||||||
{
|
{
|
||||||
|
@ -90,7 +90,7 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(TextureStore textureStore, Storyboard storyboard)
|
private void load(TextureStore textureStore, Storyboard storyboard)
|
||||||
{
|
{
|
||||||
Texture = storyboard.GetTextureFromPath(Sprite.Path, textureStore);
|
Texture = textureStore.Get(Sprite.Path);
|
||||||
|
|
||||||
if (Texture == null && storyboard.UseSkinSprites)
|
if (Texture == null && storyboard.UseSkinSprites)
|
||||||
{
|
{
|
||||||
|
@ -29,12 +29,7 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(IBindable<WorkingBeatmap> beatmap, TextureStore textureStore)
|
private void load(IBindable<WorkingBeatmap> beatmap, TextureStore textureStore)
|
||||||
{
|
{
|
||||||
string? path = beatmap.Value.BeatmapSetInfo?.GetPathForFile(Video.Path);
|
var stream = textureStore.GetStream(Video.Path);
|
||||||
|
|
||||||
if (path == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var stream = textureStore.GetStream(path);
|
|
||||||
|
|
||||||
if (stream == null)
|
if (stream == null)
|
||||||
return;
|
return;
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Graphics.Textures;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Storyboards.Drawables;
|
using osu.Game.Storyboards.Drawables;
|
||||||
@ -92,7 +91,7 @@ namespace osu.Game.Storyboards
|
|||||||
|
|
||||||
private static readonly string[] image_extensions = { @".png", @".jpg" };
|
private static readonly string[] image_extensions = { @".png", @".jpg" };
|
||||||
|
|
||||||
public Texture? GetTextureFromPath(string path, TextureStore textureStore)
|
public virtual string? GetStoragePathFromStoryboardPath(string path)
|
||||||
{
|
{
|
||||||
string? resolvedPath = null;
|
string? resolvedPath = null;
|
||||||
|
|
||||||
@ -102,10 +101,7 @@ namespace osu.Game.Storyboards
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Just doing this extension logic locally here for simplicity.
|
// Some old storyboards don't include a file extension, so let's best guess at one.
|
||||||
//
|
|
||||||
// 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?).
|
|
||||||
foreach (string ext in image_extensions)
|
foreach (string ext in image_extensions)
|
||||||
{
|
{
|
||||||
if ((resolvedPath = BeatmapInfo.BeatmapSet?.GetPathForFile($"{path}{ext}")) != null)
|
if ((resolvedPath = BeatmapInfo.BeatmapSet?.GetPathForFile($"{path}{ext}")) != null)
|
||||||
@ -113,10 +109,7 @@ namespace osu.Game.Storyboards
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(resolvedPath))
|
return resolvedPath;
|
||||||
return textureStore.Get(resolvedPath);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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)
|
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);
|
return MessagePackSerializer.Deserialize<T>(serialized, SignalRUnionWorkaroundResolver.OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,7 +265,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
{
|
{
|
||||||
Debug.Assert(original.BeatmapSet != null);
|
Debug.Assert(original.BeatmapSet != null);
|
||||||
|
|
||||||
return new APIBeatmapSet
|
var result = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
OnlineID = original.BeatmapSet.OnlineID,
|
OnlineID = original.BeatmapSet.OnlineID,
|
||||||
Status = BeatmapOnlineStatus.Ranked,
|
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) =>
|
protected WorkingBeatmap CreateWorkingBeatmap(RulesetInfo ruleset) =>
|
||||||
|
@ -12,6 +12,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual
|
namespace osu.Game.Tests.Visual
|
||||||
{
|
{
|
||||||
@ -79,6 +80,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
protected void LoadPlayer(Mod[] mods)
|
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();
|
var ruleset = CreatePlayerRuleset();
|
||||||
Ruleset.Value = ruleset.RulesetInfo;
|
Ruleset.Value = ruleset.RulesetInfo;
|
||||||
|
|
||||||
|
@ -21,13 +21,13 @@
|
|||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||||
<PackageReference Include="DiffPlex" Version="1.7.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="Humanizer" Version="2.14.1" />
|
||||||
<PackageReference Include="MessagePack" Version="2.4.59" />
|
<PackageReference Include="MessagePack" Version="2.5.124" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="7.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="7.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="7.0.2" />
|
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="7.0.11" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" />
|
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
@ -35,13 +35,13 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</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.Framework" Version="2023.914.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.914.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.914.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.28.1" />
|
<PackageReference Include="Sentry" Version="3.39.1" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<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="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||||
|
|
||||||
|
@ -34,9 +34,9 @@
|
|||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>We don't really use the camera.</string>
|
<string>We don't really use the camera.</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>We don't really use the microphone.</string>
|
<string>We don't really use the microphone.</string>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
@ -130,5 +130,7 @@
|
|||||||
<string>Editor</string>
|
<string>Editor</string>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.music-games</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
Loading…
Reference in New Issue
Block a user