mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 10:18:22 +08:00
Merge pull request #26630 from peppy/s-rank-change
Change S rank to require no miss
This commit is contained in:
commit
b272d34960
@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
|||||||
return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
|
return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override ScoreRank RankFromAccuracy(double accuracy)
|
public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary<HitResult, int> results)
|
||||||
{
|
{
|
||||||
if (accuracy == accuracy_cutoff_x)
|
if (accuracy == accuracy_cutoff_x)
|
||||||
return ScoreRank.X;
|
return ScoreRank.X;
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Scoring
|
namespace osu.Game.Rulesets.Osu.Scoring
|
||||||
{
|
{
|
||||||
@ -14,6 +16,22 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary<HitResult, int> results)
|
||||||
|
{
|
||||||
|
ScoreRank rank = base.RankFromScore(accuracy, results);
|
||||||
|
|
||||||
|
switch (rank)
|
||||||
|
{
|
||||||
|
case ScoreRank.S:
|
||||||
|
case ScoreRank.X:
|
||||||
|
if (results.GetValueOrDefault(HitResult.Miss) > 0)
|
||||||
|
rank = ScoreRank.A;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rank;
|
||||||
|
}
|
||||||
|
|
||||||
protected override HitEvent CreateHitEvent(JudgementResult result)
|
protected override HitEvent CreateHitEvent(JudgementResult result)
|
||||||
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
|
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
// 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.Collections.Generic;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Scoring
|
namespace osu.Game.Rulesets.Taiko.Scoring
|
||||||
{
|
{
|
||||||
@ -33,6 +35,22 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
|||||||
* strongScaleValue(result);
|
* strongScaleValue(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary<HitResult, int> results)
|
||||||
|
{
|
||||||
|
ScoreRank rank = base.RankFromScore(accuracy, results);
|
||||||
|
|
||||||
|
switch (rank)
|
||||||
|
{
|
||||||
|
case ScoreRank.S:
|
||||||
|
case ScoreRank.X:
|
||||||
|
if (results.GetValueOrDefault(HitResult.Miss) > 0)
|
||||||
|
rank = ScoreRank.A;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rank;
|
||||||
|
}
|
||||||
|
|
||||||
public override int GetBaseScoreForResult(HitResult result)
|
public override int GetBaseScoreForResult(HitResult result)
|
||||||
{
|
{
|
||||||
switch (result)
|
switch (result)
|
||||||
|
@ -10,7 +10,6 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.Formats;
|
using osu.Game.Beatmaps.Formats;
|
||||||
using osu.Game.Beatmaps.Legacy;
|
using osu.Game.Beatmaps.Legacy;
|
||||||
@ -23,6 +22,7 @@ using osu.Game.Rulesets.Mania.Mods;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Replays;
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
@ -59,14 +59,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]);
|
Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]);
|
||||||
Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]);
|
Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]);
|
||||||
|
|
||||||
Assert.AreEqual(829_931, score.ScoreInfo.TotalScore);
|
Assert.AreEqual(829_931, score.ScoreInfo.LegacyTotalScore);
|
||||||
Assert.AreEqual(3, score.ScoreInfo.MaxCombo);
|
Assert.AreEqual(3, score.ScoreInfo.MaxCombo);
|
||||||
|
|
||||||
Assert.IsTrue(score.ScoreInfo.Mods.Any(m => m is ManiaModClassic));
|
Assert.IsTrue(score.ScoreInfo.Mods.Any(m => m is ManiaModClassic));
|
||||||
Assert.IsTrue(score.ScoreInfo.APIMods.Any(m => m.Acronym == "CL"));
|
Assert.IsTrue(score.ScoreInfo.APIMods.Any(m => m.Acronym == "CL"));
|
||||||
Assert.IsTrue(score.ScoreInfo.ModsJson.Contains("CL"));
|
Assert.IsTrue(score.ScoreInfo.ModsJson.Contains("CL"));
|
||||||
|
|
||||||
Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001));
|
Assert.That((2 * 300d + 1 * 200) / (3 * 305d), Is.EqualTo(score.ScoreInfo.Accuracy).Within(0.0001));
|
||||||
Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank);
|
Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank);
|
||||||
|
|
||||||
Assert.That(score.Replay.Frames, Is.Not.Empty);
|
Assert.That(score.Replay.Frames, Is.Not.Empty);
|
||||||
@ -252,7 +252,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void AccuracyAndRankOfStableScorePreserved()
|
public void AccuracyOfStableScoreRecomputed()
|
||||||
{
|
{
|
||||||
var memoryStream = new MemoryStream();
|
var memoryStream = new MemoryStream();
|
||||||
|
|
||||||
@ -261,15 +261,16 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
// and we want to emulate a stable score here
|
// and we want to emulate a stable score here
|
||||||
using (var sw = new SerializationWriter(memoryStream, true))
|
using (var sw = new SerializationWriter(memoryStream, true))
|
||||||
{
|
{
|
||||||
sw.Write((byte)0); // ruleset id (osu!)
|
sw.Write((byte)3); // ruleset id (mania).
|
||||||
|
// mania is used intentionally as it is the only ruleset wherein default accuracy calculation is changed in lazer
|
||||||
sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable)
|
sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable)
|
||||||
sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test
|
sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test
|
||||||
sw.Write("username"); // irrelevant to this test
|
sw.Write("username"); // irrelevant to this test
|
||||||
sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test
|
sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test
|
||||||
sw.Write((ushort)198); // count300
|
sw.Write((ushort)1); // count300
|
||||||
sw.Write((ushort)1); // count100
|
sw.Write((ushort)0); // count100
|
||||||
sw.Write((ushort)0); // count50
|
sw.Write((ushort)0); // count50
|
||||||
sw.Write((ushort)0); // countGeki
|
sw.Write((ushort)198); // countGeki (perfects / "rainbow 300s" in mania)
|
||||||
sw.Write((ushort)0); // countKatu
|
sw.Write((ushort)0); // countKatu
|
||||||
sw.Write((ushort)1); // countMiss
|
sw.Write((ushort)1); // countMiss
|
||||||
sw.Write(12345678); // total score, irrelevant to this test
|
sw.Write(12345678); // total score, irrelevant to this test
|
||||||
@ -287,13 +288,54 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 300 + 100) / (200 * 300)));
|
Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 305 + 300) / (200 * 305)));
|
||||||
Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A));
|
Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void AccuracyAndRankOfLazerScorePreserved()
|
public void RankOfStableScoreUsesLazerDefinitions()
|
||||||
|
{
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
|
||||||
|
// local partial implementation of legacy score encoder
|
||||||
|
// this is done half for readability, half because `LegacyScoreEncoder` forces `LATEST_VERSION`
|
||||||
|
// and we want to emulate a stable score here
|
||||||
|
using (var sw = new SerializationWriter(memoryStream, true))
|
||||||
|
{
|
||||||
|
sw.Write((byte)0); // ruleset id (osu!)
|
||||||
|
sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable)
|
||||||
|
sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test
|
||||||
|
sw.Write("username"); // irrelevant to this test
|
||||||
|
sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test
|
||||||
|
sw.Write((ushort)195); // count300
|
||||||
|
sw.Write((ushort)1); // count100
|
||||||
|
sw.Write((ushort)4); // count50
|
||||||
|
sw.Write((ushort)0); // countGeki
|
||||||
|
sw.Write((ushort)0); // countKatu
|
||||||
|
sw.Write((ushort)0); // countMiss
|
||||||
|
sw.Write(12345678); // total score, irrelevant to this test
|
||||||
|
sw.Write((ushort)1000); // max combo, irrelevant to this test
|
||||||
|
sw.Write(false); // full combo, irrelevant to this test
|
||||||
|
sw.Write((int)LegacyMods.Hidden); // mods
|
||||||
|
sw.Write(string.Empty); // hp graph, irrelevant
|
||||||
|
sw.Write(DateTime.Now); // date, irrelevant
|
||||||
|
sw.Write(Array.Empty<byte>()); // replay data, irrelevant
|
||||||
|
sw.Write((long)1234); // legacy online ID, irrelevant
|
||||||
|
}
|
||||||
|
|
||||||
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
var decoded = new TestLegacyScoreDecoder().Parse(memoryStream);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
// In stable this would be an A because there are over 1% 50s. But that's not a thing in lazer.
|
||||||
|
Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AccuracyRankAndTotalScoreOfLazerScorePreserved()
|
||||||
{
|
{
|
||||||
var ruleset = new OsuRuleset().RulesetInfo;
|
var ruleset = new OsuRuleset().RulesetInfo;
|
||||||
|
|
||||||
@ -321,8 +363,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
|
Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(284_537));
|
||||||
|
Assert.That(decodedAfterEncode.ScoreInfo.LegacyTotalScore, Is.Null);
|
||||||
Assert.That(decodedAfterEncode.ScoreInfo.Accuracy, Is.EqualTo((double)(199 * 300 + 30) / (200 * 300 + 30)));
|
Assert.That(decodedAfterEncode.ScoreInfo.Accuracy, Is.EqualTo((double)(199 * 300 + 30) / (200 * 300 + 30)));
|
||||||
Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH));
|
Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,6 +459,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
Ruleset = new OsuRuleset().RulesetInfo,
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
Difficulty = new BeatmapDifficulty(),
|
Difficulty = new BeatmapDifficulty(),
|
||||||
BeatmapVersion = beatmapVersion,
|
BeatmapVersion = beatmapVersion,
|
||||||
|
},
|
||||||
|
// needs to have at least one objects so that `StandardisedScoreMigrationTools` doesn't die
|
||||||
|
// when trying to recompute total score.
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Scoring.Legacy;
|
using osu.Game.Scoring.Legacy;
|
||||||
@ -210,31 +211,6 @@ namespace osu.Game.Tests.Database
|
|||||||
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000001));
|
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000001));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestNonLegacyScoreNotSubjectToUpgrades()
|
|
||||||
{
|
|
||||||
ScoreInfo scoreInfo = null!;
|
|
||||||
TestBackgroundDataStoreProcessor processor = null!;
|
|
||||||
|
|
||||||
AddStep("Add score which requires upgrade (and has beatmap)", () =>
|
|
||||||
{
|
|
||||||
Realm.Write(r =>
|
|
||||||
{
|
|
||||||
r.Add(scoreInfo = new ScoreInfo(ruleset: r.All<RulesetInfo>().First(), beatmap: r.All<BeatmapInfo>().First())
|
|
||||||
{
|
|
||||||
TotalScoreVersion = 30000005,
|
|
||||||
LegacyTotalScore = 123456,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
|
|
||||||
AddUntilStep("Wait for completion", () => processor.Completed);
|
|
||||||
|
|
||||||
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
|
|
||||||
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000005));
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor
|
public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor
|
||||||
{
|
{
|
||||||
protected override int TimeToSleepDuringGameplay => 10;
|
protected override int TimeToSleepDuringGameplay => 10;
|
||||||
|
@ -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.Collections.Generic;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -96,6 +97,14 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
{
|
{
|
||||||
var scoreProcessor = ruleset.CreateScoreProcessor();
|
var scoreProcessor = ruleset.CreateScoreProcessor();
|
||||||
|
|
||||||
|
var statistics = new Dictionary<HitResult, int>
|
||||||
|
{
|
||||||
|
{ HitResult.Miss, 1 },
|
||||||
|
{ HitResult.Meh, 50 },
|
||||||
|
{ HitResult.Good, 100 },
|
||||||
|
{ HitResult.Great, 300 },
|
||||||
|
};
|
||||||
|
|
||||||
return new ScoreInfo
|
return new ScoreInfo
|
||||||
{
|
{
|
||||||
User = new APIUser
|
User = new APIUser
|
||||||
@ -109,15 +118,9 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
TotalScore = 2845370,
|
TotalScore = 2845370,
|
||||||
Accuracy = accuracy,
|
Accuracy = accuracy,
|
||||||
MaxCombo = 999,
|
MaxCombo = 999,
|
||||||
Rank = scoreProcessor.RankFromAccuracy(accuracy),
|
Rank = scoreProcessor.RankFromScore(accuracy, statistics),
|
||||||
Date = DateTimeOffset.Now,
|
Date = DateTimeOffset.Now,
|
||||||
Statistics =
|
Statistics = statistics,
|
||||||
{
|
|
||||||
{ HitResult.Miss, 1 },
|
|
||||||
{ HitResult.Meh, 50 },
|
|
||||||
{ HitResult.Good, 100 },
|
|
||||||
{ HitResult.Great, 300 },
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ using osu.Game.Online.API;
|
|||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens;
|
using osu.Game.Screens;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
@ -71,15 +72,16 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
|
|
||||||
private int onlineScoreID = 1;
|
private int onlineScoreID = 1;
|
||||||
|
|
||||||
[TestCase(1, ScoreRank.X)]
|
[TestCase(1, ScoreRank.X, 0)]
|
||||||
[TestCase(0.9999, ScoreRank.S)]
|
[TestCase(0.9999, ScoreRank.S, 0)]
|
||||||
[TestCase(0.975, ScoreRank.S)]
|
[TestCase(0.975, ScoreRank.S, 0)]
|
||||||
[TestCase(0.925, ScoreRank.A)]
|
[TestCase(0.975, ScoreRank.A, 1)]
|
||||||
[TestCase(0.85, ScoreRank.B)]
|
[TestCase(0.925, ScoreRank.A, 5)]
|
||||||
[TestCase(0.75, ScoreRank.C)]
|
[TestCase(0.85, ScoreRank.B, 9)]
|
||||||
[TestCase(0.5, ScoreRank.D)]
|
[TestCase(0.75, ScoreRank.C, 11)]
|
||||||
[TestCase(0.2, ScoreRank.D)]
|
[TestCase(0.5, ScoreRank.D, 21)]
|
||||||
public void TestResultsWithPlayer(double accuracy, ScoreRank rank)
|
[TestCase(0.2, ScoreRank.D, 51)]
|
||||||
|
public void TestResultsWithPlayer(double accuracy, ScoreRank rank, int missCount)
|
||||||
{
|
{
|
||||||
TestResultsScreen screen = null;
|
TestResultsScreen screen = null;
|
||||||
|
|
||||||
@ -91,6 +93,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents();
|
score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents();
|
||||||
score.Accuracy = accuracy;
|
score.Accuracy = accuracy;
|
||||||
score.Rank = rank;
|
score.Rank = rank;
|
||||||
|
score.Statistics[HitResult.Miss] = missCount;
|
||||||
|
|
||||||
return screen = createResultsScreen(score);
|
return screen = createResultsScreen(score);
|
||||||
});
|
});
|
||||||
|
@ -12,7 +12,6 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Database;
|
|
||||||
using osu.Game.Extensions;
|
using osu.Game.Extensions;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
@ -22,7 +21,7 @@ using osu.Game.Scoring;
|
|||||||
using osu.Game.Scoring.Legacy;
|
using osu.Game.Scoring.Legacy;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
namespace osu.Game
|
namespace osu.Game.Database
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs background updating of data stores at startup.
|
/// Performs background updating of data stores at startup.
|
||||||
@ -74,6 +73,7 @@ namespace osu.Game
|
|||||||
processBeatmapsWithMissingObjectCounts();
|
processBeatmapsWithMissingObjectCounts();
|
||||||
processScoresWithMissingStatistics();
|
processScoresWithMissingStatistics();
|
||||||
convertLegacyTotalScoreToStandardised();
|
convertLegacyTotalScoreToStandardised();
|
||||||
|
upgradeScoreRanks();
|
||||||
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
|
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
|
||||||
{
|
{
|
||||||
if (t.Exception?.InnerException is ObjectDisposedException)
|
if (t.Exception?.InnerException is ObjectDisposedException)
|
||||||
@ -355,7 +355,7 @@ namespace osu.Game
|
|||||||
realmAccess.Write(r =>
|
realmAccess.Write(r =>
|
||||||
{
|
{
|
||||||
ScoreInfo s = r.Find<ScoreInfo>(id)!;
|
ScoreInfo s = r.Find<ScoreInfo>(id)!;
|
||||||
StandardisedScoreMigrationTools.UpdateFromLegacy(s, beatmapManager);
|
StandardisedScoreMigrationTools.UpdateFromLegacy(s, beatmapManager.GetWorkingBeatmap(s.BeatmapInfo));
|
||||||
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
|
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -376,6 +376,66 @@ namespace osu.Game
|
|||||||
completeNotification(notification, processedCount, scoreIds.Count, failedCount);
|
completeNotification(notification, processedCount, scoreIds.Count, failedCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void upgradeScoreRanks()
|
||||||
|
{
|
||||||
|
Logger.Log("Querying for scores that need rank upgrades...");
|
||||||
|
|
||||||
|
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(
|
||||||
|
r.All<ScoreInfo>()
|
||||||
|
.Where(s => s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION)
|
||||||
|
.AsEnumerable()
|
||||||
|
// must be done after materialisation, as realm doesn't support
|
||||||
|
// filtering on nested property predicates or projection via `.Select()`
|
||||||
|
.Where(s => s.Ruleset.IsLegacyRuleset())
|
||||||
|
.Select(s => s.ID)));
|
||||||
|
|
||||||
|
Logger.Log($"Found {scoreIds.Count} scores which require rank upgrades.");
|
||||||
|
|
||||||
|
if (scoreIds.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var notification = showProgressNotification(scoreIds.Count, "Adjusting ranks of scores", "scores now have more correct ranks");
|
||||||
|
|
||||||
|
int processedCount = 0;
|
||||||
|
int failedCount = 0;
|
||||||
|
|
||||||
|
foreach (var id in scoreIds)
|
||||||
|
{
|
||||||
|
if (notification?.State == ProgressNotificationState.Cancelled)
|
||||||
|
break;
|
||||||
|
|
||||||
|
updateNotificationProgress(notification, processedCount, scoreIds.Count);
|
||||||
|
|
||||||
|
sleepIfRequired();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Can't use async overload because we're not on the update thread.
|
||||||
|
// ReSharper disable once MethodHasAsyncOverload
|
||||||
|
realmAccess.Write(r =>
|
||||||
|
{
|
||||||
|
ScoreInfo s = r.Find<ScoreInfo>(id)!;
|
||||||
|
s.Rank = StandardisedScoreMigrationTools.ComputeRank(s);
|
||||||
|
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
|
||||||
|
});
|
||||||
|
|
||||||
|
++processedCount;
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Log($"Failed to update rank score {id}: {e}");
|
||||||
|
realmAccess.Write(r => r.Find<ScoreInfo>(id)!.BackgroundReprocessingFailed = true);
|
||||||
|
++failedCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completeNotification(notification, processedCount, scoreIds.Count, failedCount);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount)
|
private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount)
|
||||||
{
|
{
|
||||||
if (notification == null)
|
if (notification == null)
|
@ -232,42 +232,60 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates a legacy <see cref="ScoreInfo"/> to standardised scoring.
|
/// Updates a <see cref="ScoreInfo"/> to standardised scoring.
|
||||||
|
/// This will recompite the score's <see cref="ScoreInfo.Accuracy"/> (always), <see cref="ScoreInfo.Rank"/> (always),
|
||||||
|
/// and <see cref="ScoreInfo.TotalScore"/> (if the score comes from stable).
|
||||||
|
/// The total score from stable - if any applicable - will be stored to <see cref="ScoreInfo.LegacyTotalScore"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="score">The score to update.</param>
|
/// <param name="score">The score to update.</param>
|
||||||
/// <param name="beatmaps">A <see cref="BeatmapManager"/> used for <see cref="WorkingBeatmap"/> lookups.</param>
|
/// <param name="beatmap">The <see cref="WorkingBeatmap"/> applicable for this score.</param>
|
||||||
public static void UpdateFromLegacy(ScoreInfo score, BeatmapManager beatmaps)
|
public static void UpdateFromLegacy(ScoreInfo score, WorkingBeatmap beatmap)
|
||||||
{
|
{
|
||||||
score.TotalScore = convertFromLegacyTotalScore(score, beatmaps);
|
var ruleset = score.Ruleset.CreateInstance();
|
||||||
score.Accuracy = ComputeAccuracy(score);
|
var scoreProcessor = ruleset.CreateScoreProcessor();
|
||||||
|
|
||||||
|
// warning: ordering is important here - both total score and ranks are dependent on accuracy!
|
||||||
|
score.Accuracy = computeAccuracy(score, scoreProcessor);
|
||||||
|
score.Rank = computeRank(score, scoreProcessor);
|
||||||
|
score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates a legacy <see cref="ScoreInfo"/> to standardised scoring.
|
/// Updates a <see cref="ScoreInfo"/> to standardised scoring.
|
||||||
|
/// This will recompute the score's <see cref="ScoreInfo.Accuracy"/> (always), <see cref="ScoreInfo.Rank"/> (always),
|
||||||
|
/// and <see cref="ScoreInfo.TotalScore"/> (if the score comes from stable).
|
||||||
|
/// The total score from stable - if any applicable - will be stored to <see cref="ScoreInfo.LegacyTotalScore"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This overload is intended for server-side flows.
|
||||||
|
/// See: https://github.com/ppy/osu-queue-score-statistics/blob/3681e92ac91c6c61922094bdbc7e92e6217dd0fc/osu.Server.Queues.ScoreStatisticsProcessor/Commands/Queue/BatchInserter.cs
|
||||||
|
/// </remarks>
|
||||||
/// <param name="score">The score to update.</param>
|
/// <param name="score">The score to update.</param>
|
||||||
|
/// <param name="ruleset">The <see cref="Ruleset"/> in which the score was set.</param>
|
||||||
/// <param name="difficulty">The beatmap difficulty.</param>
|
/// <param name="difficulty">The beatmap difficulty.</param>
|
||||||
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
|
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
|
||||||
public static void UpdateFromLegacy(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes)
|
public static void UpdateFromLegacy(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes)
|
||||||
{
|
{
|
||||||
score.TotalScore = convertFromLegacyTotalScore(score, difficulty, attributes);
|
var scoreProcessor = ruleset.CreateScoreProcessor();
|
||||||
score.Accuracy = ComputeAccuracy(score);
|
|
||||||
|
// warning: ordering is important here - both total score and ranks are dependent on accuracy!
|
||||||
|
score.Accuracy = computeAccuracy(score, scoreProcessor);
|
||||||
|
score.Rank = computeRank(score, scoreProcessor);
|
||||||
|
score.TotalScore = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts from <see cref="ScoreInfo.LegacyTotalScore"/> to the new standardised scoring of <see cref="ScoreProcessor"/>.
|
/// Converts from <see cref="ScoreInfo.LegacyTotalScore"/> to the new standardised scoring of <see cref="ScoreProcessor"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="score">The score to convert the total score of.</param>
|
/// <param name="score">The score to convert the total score of.</param>
|
||||||
/// <param name="beatmaps">A <see cref="BeatmapManager"/> used for <see cref="WorkingBeatmap"/> lookups.</param>
|
/// <param name="ruleset">The <see cref="Ruleset"/> in which the score was set.</param>
|
||||||
|
/// <param name="beatmap">The <see cref="WorkingBeatmap"/> applicable for this score.</param>
|
||||||
/// <returns>The standardised total score.</returns>
|
/// <returns>The standardised total score.</returns>
|
||||||
private static long convertFromLegacyTotalScore(ScoreInfo score, BeatmapManager beatmaps)
|
private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap)
|
||||||
{
|
{
|
||||||
if (!score.IsLegacyScore)
|
if (!score.IsLegacyScore)
|
||||||
return score.TotalScore;
|
return score.TotalScore;
|
||||||
|
|
||||||
WorkingBeatmap beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo);
|
|
||||||
Ruleset ruleset = score.Ruleset.CreateInstance();
|
|
||||||
|
|
||||||
if (ruleset is not ILegacyRuleset legacyRuleset)
|
if (ruleset is not ILegacyRuleset legacyRuleset)
|
||||||
return score.TotalScore;
|
return score.TotalScore;
|
||||||
|
|
||||||
@ -283,24 +301,24 @@ namespace osu.Game.Database
|
|||||||
ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator();
|
ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator();
|
||||||
LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap);
|
LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap);
|
||||||
|
|
||||||
return convertFromLegacyTotalScore(score, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes);
|
return convertFromLegacyTotalScore(score, ruleset, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts from <see cref="ScoreInfo.LegacyTotalScore"/> to the new standardised scoring of <see cref="ScoreProcessor"/>.
|
/// Converts from <see cref="ScoreInfo.LegacyTotalScore"/> to the new standardised scoring of <see cref="ScoreProcessor"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="score">The score to convert the total score of.</param>
|
/// <param name="score">The score to convert the total score of.</param>
|
||||||
|
/// <param name="ruleset">The <see cref="Ruleset"/> in which the score was set.</param>
|
||||||
/// <param name="difficulty">The beatmap difficulty.</param>
|
/// <param name="difficulty">The beatmap difficulty.</param>
|
||||||
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
|
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
|
||||||
/// <returns>The standardised total score.</returns>
|
/// <returns>The standardised total score.</returns>
|
||||||
private static long convertFromLegacyTotalScore(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes)
|
private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes)
|
||||||
{
|
{
|
||||||
if (!score.IsLegacyScore)
|
if (!score.IsLegacyScore)
|
||||||
return score.TotalScore;
|
return score.TotalScore;
|
||||||
|
|
||||||
Debug.Assert(score.LegacyTotalScore != null);
|
Debug.Assert(score.LegacyTotalScore != null);
|
||||||
|
|
||||||
Ruleset ruleset = score.Ruleset.CreateInstance();
|
|
||||||
if (ruleset is not ILegacyRuleset legacyRuleset)
|
if (ruleset is not ILegacyRuleset legacyRuleset)
|
||||||
return score.TotalScore;
|
return score.TotalScore;
|
||||||
|
|
||||||
@ -474,14 +492,9 @@ namespace osu.Game.Database
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 3:
|
case 3:
|
||||||
// in the mania case accuracy actually changes between score V1 and score V2 / standardised
|
|
||||||
// (PERFECT weighting changes from 300 to 305),
|
|
||||||
// so for better accuracy recompute accuracy locally based on hit statistics and use that instead,
|
|
||||||
double scoreV2Accuracy = ComputeAccuracy(score);
|
|
||||||
|
|
||||||
convertedTotalScore = (long)Math.Round((
|
convertedTotalScore = (long)Math.Round((
|
||||||
850000 * comboProportion
|
850000 * comboProportion
|
||||||
+ 150000 * Math.Pow(scoreV2Accuracy, 2 + 2 * scoreV2Accuracy)
|
+ 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy)
|
||||||
+ bonusProportion) * modMultiplier);
|
+ bonusProportion) * modMultiplier);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -584,11 +597,8 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double ComputeAccuracy(ScoreInfo scoreInfo)
|
private static double computeAccuracy(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor)
|
||||||
{
|
{
|
||||||
Ruleset ruleset = scoreInfo.Ruleset.CreateInstance();
|
|
||||||
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
|
|
||||||
|
|
||||||
int baseScore = scoreInfo.Statistics.Where(kvp => kvp.Key.AffectsAccuracy())
|
int baseScore = scoreInfo.Statistics.Where(kvp => kvp.Key.AffectsAccuracy())
|
||||||
.Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key));
|
.Sum(kvp => kvp.Value * scoreProcessor.GetBaseScoreForResult(kvp.Key));
|
||||||
int maxBaseScore = scoreInfo.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy())
|
int maxBaseScore = scoreInfo.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy())
|
||||||
@ -597,6 +607,18 @@ namespace osu.Game.Database
|
|||||||
return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore;
|
return maxBaseScore == 0 ? 1 : baseScore / (double)maxBaseScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ScoreRank ComputeRank(ScoreInfo scoreInfo) => computeRank(scoreInfo, scoreInfo.Ruleset.CreateInstance().CreateScoreProcessor());
|
||||||
|
|
||||||
|
private static ScoreRank computeRank(ScoreInfo scoreInfo, ScoreProcessor scoreProcessor)
|
||||||
|
{
|
||||||
|
var rank = scoreProcessor.RankFromScore(scoreInfo.Accuracy, scoreInfo.Statistics);
|
||||||
|
|
||||||
|
foreach (var mod in scoreInfo.Mods.OfType<IApplicableToScoreProcessor>())
|
||||||
|
rank = mod.AdjustRank(rank, scoreInfo.Accuracy);
|
||||||
|
|
||||||
|
return rank;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used to populate the <paramref name="score"/> model using data parsed from its corresponding replay file.
|
/// Used to populate the <paramref name="score"/> model using data parsed from its corresponding replay file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -370,7 +370,7 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
if (rank.Value == ScoreRank.F)
|
if (rank.Value == ScoreRank.F)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
rank.Value = RankFromAccuracy(Accuracy.Value);
|
rank.Value = RankFromScore(Accuracy.Value, ScoreResultCounts);
|
||||||
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
|
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
|
||||||
rank.Value = mod.AdjustRank(Rank.Value, Accuracy.Value);
|
rank.Value = mod.AdjustRank(Rank.Value, Accuracy.Value);
|
||||||
}
|
}
|
||||||
@ -505,7 +505,7 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Given an accuracy (0..1), return the correct <see cref="ScoreRank"/>.
|
/// Given an accuracy (0..1), return the correct <see cref="ScoreRank"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual ScoreRank RankFromAccuracy(double accuracy)
|
public virtual ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary<HitResult, int> results)
|
||||||
{
|
{
|
||||||
if (accuracy == accuracy_cutoff_x)
|
if (accuracy == accuracy_cutoff_x)
|
||||||
return ScoreRank.X;
|
return ScoreRank.X;
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -19,6 +20,7 @@ using osu.Game.Replays.Legacy;
|
|||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using SharpCompress.Compressors.LZMA;
|
using SharpCompress.Compressors.LZMA;
|
||||||
|
|
||||||
namespace osu.Game.Scoring.Legacy
|
namespace osu.Game.Scoring.Legacy
|
||||||
@ -38,7 +40,6 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
};
|
};
|
||||||
|
|
||||||
WorkingBeatmap workingBeatmap;
|
WorkingBeatmap workingBeatmap;
|
||||||
byte[] compressedScoreInfo = null;
|
|
||||||
|
|
||||||
using (SerializationReader sr = new SerializationReader(stream))
|
using (SerializationReader sr = new SerializationReader(stream))
|
||||||
{
|
{
|
||||||
@ -107,6 +108,8 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
else if (version >= 20121008)
|
else if (version >= 20121008)
|
||||||
scoreInfo.LegacyOnlineID = sr.ReadInt32();
|
scoreInfo.LegacyOnlineID = sr.ReadInt32();
|
||||||
|
|
||||||
|
byte[] compressedScoreInfo = null;
|
||||||
|
|
||||||
if (version >= 30000001)
|
if (version >= 30000001)
|
||||||
compressedScoreInfo = sr.ReadByteArray();
|
compressedScoreInfo = sr.ReadByteArray();
|
||||||
|
|
||||||
@ -130,10 +133,12 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (score.ScoreInfo.IsLegacyScore || compressedScoreInfo == null)
|
PopulateMaximumStatistics(score.ScoreInfo, workingBeatmap);
|
||||||
PopulateLegacyAccuracyAndRank(score.ScoreInfo);
|
|
||||||
else
|
if (score.ScoreInfo.IsLegacyScore)
|
||||||
populateLazerAccuracyAndRank(score.ScoreInfo);
|
score.ScoreInfo.LegacyTotalScore = score.ScoreInfo.TotalScore;
|
||||||
|
|
||||||
|
StandardisedScoreMigrationTools.UpdateFromLegacy(score.ScoreInfo, workingBeatmap);
|
||||||
|
|
||||||
// before returning for database import, we must restore the database-sourced BeatmapInfo.
|
// before returning for database import, we must restore the database-sourced BeatmapInfo.
|
||||||
// if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception.
|
// if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception.
|
||||||
@ -171,121 +176,65 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Populates the accuracy of a given <see cref="ScoreInfo"/> from its contained statistics.
|
/// Populates the <see cref="ScoreInfo.MaximumStatistics"/> for a given <see cref="ScoreInfo"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <param name="score">The score to populate the statistics of.</param>
|
||||||
/// Legacy use only.
|
/// <param name="workingBeatmap">The corresponding <see cref="WorkingBeatmap"/>.</param>
|
||||||
/// </remarks>
|
internal static void PopulateMaximumStatistics(ScoreInfo score, WorkingBeatmap workingBeatmap)
|
||||||
/// <param name="score">The <see cref="ScoreInfo"/> to populate.</param>
|
|
||||||
public static void PopulateLegacyAccuracyAndRank(ScoreInfo score)
|
|
||||||
{
|
{
|
||||||
int countMiss = score.GetCountMiss() ?? 0;
|
Debug.Assert(score.BeatmapInfo != null);
|
||||||
int count50 = score.GetCount50() ?? 0;
|
|
||||||
int count100 = score.GetCount100() ?? 0;
|
|
||||||
int count300 = score.GetCount300() ?? 0;
|
|
||||||
int countGeki = score.GetCountGeki() ?? 0;
|
|
||||||
int countKatu = score.GetCountKatu() ?? 0;
|
|
||||||
|
|
||||||
switch (score.Ruleset.OnlineID)
|
if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var ruleset = score.Ruleset.Detach();
|
||||||
|
var rulesetInstance = ruleset.CreateInstance();
|
||||||
|
var scoreProcessor = rulesetInstance.CreateScoreProcessor();
|
||||||
|
|
||||||
|
// Populate the maximum statistics.
|
||||||
|
HitResult maxBasicResult = rulesetInstance.GetHitResults()
|
||||||
|
.Select(h => h.result)
|
||||||
|
.Where(h => h.IsBasic()).MaxBy(scoreProcessor.GetBaseScoreForResult);
|
||||||
|
|
||||||
|
foreach ((HitResult result, int count) in score.Statistics)
|
||||||
{
|
{
|
||||||
case 0:
|
switch (result)
|
||||||
{
|
{
|
||||||
int totalHits = count50 + count100 + count300 + countMiss;
|
case HitResult.LargeTickHit:
|
||||||
score.Accuracy = totalHits > 0 ? (double)(count50 * 50 + count100 * 100 + count300 * 300) / (totalHits * 300) : 1;
|
case HitResult.LargeTickMiss:
|
||||||
|
score.MaximumStatistics[HitResult.LargeTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.LargeTickHit) + count;
|
||||||
float ratio300 = (float)count300 / totalHits;
|
|
||||||
float ratio50 = (float)count50 / totalHits;
|
|
||||||
|
|
||||||
if (ratio300 == 1)
|
|
||||||
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X;
|
|
||||||
else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0)
|
|
||||||
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S;
|
|
||||||
else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9)
|
|
||||||
score.Rank = ScoreRank.A;
|
|
||||||
else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8)
|
|
||||||
score.Rank = ScoreRank.B;
|
|
||||||
else if (ratio300 > 0.6)
|
|
||||||
score.Rank = ScoreRank.C;
|
|
||||||
else
|
|
||||||
score.Rank = ScoreRank.D;
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case 1:
|
case HitResult.SmallTickHit:
|
||||||
{
|
case HitResult.SmallTickMiss:
|
||||||
int totalHits = count50 + count100 + count300 + countMiss;
|
score.MaximumStatistics[HitResult.SmallTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + count;
|
||||||
score.Accuracy = totalHits > 0 ? (double)(count100 * 150 + count300 * 300) / (totalHits * 300) : 1;
|
|
||||||
|
|
||||||
float ratio300 = (float)count300 / totalHits;
|
|
||||||
float ratio50 = (float)count50 / totalHits;
|
|
||||||
|
|
||||||
if (ratio300 == 1)
|
|
||||||
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X;
|
|
||||||
else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0)
|
|
||||||
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S;
|
|
||||||
else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9)
|
|
||||||
score.Rank = ScoreRank.A;
|
|
||||||
else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8)
|
|
||||||
score.Rank = ScoreRank.B;
|
|
||||||
else if (ratio300 > 0.6)
|
|
||||||
score.Rank = ScoreRank.C;
|
|
||||||
else
|
|
||||||
score.Rank = ScoreRank.D;
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case 2:
|
case HitResult.IgnoreHit:
|
||||||
{
|
case HitResult.IgnoreMiss:
|
||||||
int totalHits = count50 + count100 + count300 + countMiss + countKatu;
|
case HitResult.SmallBonus:
|
||||||
score.Accuracy = totalHits > 0 ? (double)(count50 + count100 + count300) / totalHits : 1;
|
case HitResult.LargeBonus:
|
||||||
|
|
||||||
if (score.Accuracy == 1)
|
|
||||||
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X;
|
|
||||||
else if (score.Accuracy > 0.98)
|
|
||||||
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S;
|
|
||||||
else if (score.Accuracy > 0.94)
|
|
||||||
score.Rank = ScoreRank.A;
|
|
||||||
else if (score.Accuracy > 0.9)
|
|
||||||
score.Rank = ScoreRank.B;
|
|
||||||
else if (score.Accuracy > 0.85)
|
|
||||||
score.Rank = ScoreRank.C;
|
|
||||||
else
|
|
||||||
score.Rank = ScoreRank.D;
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case 3:
|
default:
|
||||||
{
|
score.MaximumStatistics[maxBasicResult] = score.MaximumStatistics.GetValueOrDefault(maxBasicResult) + count;
|
||||||
int totalHits = count50 + count100 + count300 + countMiss + countGeki + countKatu;
|
|
||||||
score.Accuracy = totalHits > 0 ? (double)(count50 * 50 + count100 * 100 + countKatu * 200 + (count300 + countGeki) * 300) / (totalHits * 300) : 1;
|
|
||||||
|
|
||||||
if (score.Accuracy == 1)
|
|
||||||
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X;
|
|
||||||
else if (score.Accuracy > 0.95)
|
|
||||||
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S;
|
|
||||||
else if (score.Accuracy > 0.9)
|
|
||||||
score.Rank = ScoreRank.A;
|
|
||||||
else if (score.Accuracy > 0.8)
|
|
||||||
score.Rank = ScoreRank.B;
|
|
||||||
else if (score.Accuracy > 0.7)
|
|
||||||
score.Rank = ScoreRank.C;
|
|
||||||
else
|
|
||||||
score.Rank = ScoreRank.D;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void populateLazerAccuracyAndRank(ScoreInfo scoreInfo)
|
if (!score.IsLegacyScore)
|
||||||
{
|
return;
|
||||||
scoreInfo.Accuracy = StandardisedScoreMigrationTools.ComputeAccuracy(scoreInfo);
|
|
||||||
|
|
||||||
var rank = currentRuleset.CreateScoreProcessor().RankFromAccuracy(scoreInfo.Accuracy);
|
#pragma warning disable CS0618
|
||||||
|
// In osu! and osu!mania, some judgements affect combo but aren't stored to scores.
|
||||||
|
// A special hit result is used to pad out the combo value to match, based on the max combo from the difficulty attributes.
|
||||||
|
var calculator = rulesetInstance.CreateDifficultyCalculator(workingBeatmap);
|
||||||
|
var attributes = calculator.Calculate(score.Mods);
|
||||||
|
|
||||||
foreach (var mod in scoreInfo.Mods.OfType<IApplicableToScoreProcessor>())
|
int maxComboFromStatistics = score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Select(kvp => kvp.Value).DefaultIfEmpty(0).Sum();
|
||||||
rank = mod.AdjustRank(rank, scoreInfo.Accuracy);
|
if (attributes.MaxCombo > maxComboFromStatistics)
|
||||||
|
score.MaximumStatistics[HitResult.LegacyComboIncrease] = attributes.MaxCombo - maxComboFromStatistics;
|
||||||
scoreInfo.Rank = rank;
|
#pragma warning restore CS0618
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readLegacyReplay(Replay replay, StreamReader reader)
|
private void readLegacyReplay(Replay replay, StreamReader reader)
|
||||||
|
@ -43,9 +43,10 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
/// 30000012: Fix incorrect total score conversion on selected beatmaps after implementing the more correct
|
/// 30000012: Fix incorrect total score conversion on selected beatmaps after implementing the more correct
|
||||||
/// <see cref="LegacyRulesetExtensions.CalculateDifficultyPeppyStars"/> method. Reconvert all scores.
|
/// <see cref="LegacyRulesetExtensions.CalculateDifficultyPeppyStars"/> method. Reconvert all scores.
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
|
/// <item><description>30000013: All local scores will use lazer definitions of ranks for consistency. Recalculates the rank of all scores.</description></item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public const int LATEST_VERSION = 30000012;
|
public const int LATEST_VERSION = 30000013;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
|
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
|
||||||
|
@ -17,7 +17,6 @@ using osu.Game.Scoring.Legacy;
|
|||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Online.API.Requests;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Rulesets.Scoring;
|
|
||||||
using Realms;
|
using Realms;
|
||||||
|
|
||||||
namespace osu.Game.Scoring
|
namespace osu.Game.Scoring
|
||||||
@ -91,8 +90,6 @@ namespace osu.Game.Scoring
|
|||||||
ArgumentNullException.ThrowIfNull(model.BeatmapInfo);
|
ArgumentNullException.ThrowIfNull(model.BeatmapInfo);
|
||||||
ArgumentNullException.ThrowIfNull(model.Ruleset);
|
ArgumentNullException.ThrowIfNull(model.Ruleset);
|
||||||
|
|
||||||
PopulateMaximumStatistics(model);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(model.StatisticsJson))
|
if (string.IsNullOrEmpty(model.StatisticsJson))
|
||||||
model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics);
|
model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics);
|
||||||
|
|
||||||
@ -103,75 +100,6 @@ namespace osu.Game.Scoring
|
|||||||
// this requires: max combo, statistics, max statistics (where available), and mods to already be populated on the score.
|
// this requires: max combo, statistics, max statistics (where available), and mods to already be populated on the score.
|
||||||
if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model))
|
if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model))
|
||||||
model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model);
|
model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model);
|
||||||
else if (model.IsLegacyScore)
|
|
||||||
{
|
|
||||||
model.LegacyTotalScore = model.TotalScore;
|
|
||||||
StandardisedScoreMigrationTools.UpdateFromLegacy(model, beatmaps());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Populates the <see cref="ScoreInfo.MaximumStatistics"/> for a given <see cref="ScoreInfo"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="score">The score to populate the statistics of.</param>
|
|
||||||
public void PopulateMaximumStatistics(ScoreInfo score)
|
|
||||||
{
|
|
||||||
Debug.Assert(score.BeatmapInfo != null);
|
|
||||||
|
|
||||||
if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var beatmap = score.BeatmapInfo!.Detach();
|
|
||||||
var ruleset = score.Ruleset.Detach();
|
|
||||||
var rulesetInstance = ruleset.CreateInstance();
|
|
||||||
var scoreProcessor = rulesetInstance.CreateScoreProcessor();
|
|
||||||
|
|
||||||
Debug.Assert(rulesetInstance != null);
|
|
||||||
|
|
||||||
// Populate the maximum statistics.
|
|
||||||
HitResult maxBasicResult = rulesetInstance.GetHitResults()
|
|
||||||
.Select(h => h.result)
|
|
||||||
.Where(h => h.IsBasic()).MaxBy(scoreProcessor.GetBaseScoreForResult);
|
|
||||||
|
|
||||||
foreach ((HitResult result, int count) in score.Statistics)
|
|
||||||
{
|
|
||||||
switch (result)
|
|
||||||
{
|
|
||||||
case HitResult.LargeTickHit:
|
|
||||||
case HitResult.LargeTickMiss:
|
|
||||||
score.MaximumStatistics[HitResult.LargeTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.LargeTickHit) + count;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HitResult.SmallTickHit:
|
|
||||||
case HitResult.SmallTickMiss:
|
|
||||||
score.MaximumStatistics[HitResult.SmallTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + count;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HitResult.IgnoreHit:
|
|
||||||
case HitResult.IgnoreMiss:
|
|
||||||
case HitResult.SmallBonus:
|
|
||||||
case HitResult.LargeBonus:
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
score.MaximumStatistics[maxBasicResult] = score.MaximumStatistics.GetValueOrDefault(maxBasicResult) + count;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!score.IsLegacyScore)
|
|
||||||
return;
|
|
||||||
|
|
||||||
#pragma warning disable CS0618
|
|
||||||
// In osu! and osu!mania, some judgements affect combo but aren't stored to scores.
|
|
||||||
// A special hit result is used to pad out the combo value to match, based on the max combo from the difficulty attributes.
|
|
||||||
var calculator = rulesetInstance.CreateDifficultyCalculator(beatmaps().GetWorkingBeatmap(beatmap));
|
|
||||||
var attributes = calculator.Calculate(score.Mods);
|
|
||||||
|
|
||||||
int maxComboFromStatistics = score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Select(kvp => kvp.Value).DefaultIfEmpty(0).Sum();
|
|
||||||
if (attributes.MaxCombo > maxComboFromStatistics)
|
|
||||||
score.MaximumStatistics[HitResult.LegacyComboIncrease] = attributes.MaxCombo - maxComboFromStatistics;
|
|
||||||
#pragma warning restore CS0618
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores).
|
// Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores).
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -26,6 +27,7 @@ namespace osu.Game.Scoring
|
|||||||
{
|
{
|
||||||
public class ScoreManager : ModelManager<ScoreInfo>, IModelImporter<ScoreInfo>
|
public class ScoreManager : ModelManager<ScoreInfo>, IModelImporter<ScoreInfo>
|
||||||
{
|
{
|
||||||
|
private readonly Func<BeatmapManager> beatmaps;
|
||||||
private readonly OsuConfigManager configManager;
|
private readonly OsuConfigManager configManager;
|
||||||
private readonly ScoreImporter scoreImporter;
|
private readonly ScoreImporter scoreImporter;
|
||||||
private readonly LegacyScoreExporter scoreExporter;
|
private readonly LegacyScoreExporter scoreExporter;
|
||||||
@ -44,6 +46,7 @@ namespace osu.Game.Scoring
|
|||||||
OsuConfigManager configManager = null)
|
OsuConfigManager configManager = null)
|
||||||
: base(storage, realm)
|
: base(storage, realm)
|
||||||
{
|
{
|
||||||
|
this.beatmaps = beatmaps;
|
||||||
this.configManager = configManager;
|
this.configManager = configManager;
|
||||||
|
|
||||||
scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm, api)
|
scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm, api)
|
||||||
@ -171,7 +174,11 @@ namespace osu.Game.Scoring
|
|||||||
/// Populates the <see cref="ScoreInfo.MaximumStatistics"/> for a given <see cref="ScoreInfo"/>.
|
/// Populates the <see cref="ScoreInfo.MaximumStatistics"/> for a given <see cref="ScoreInfo"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="score">The score to populate the statistics of.</param>
|
/// <param name="score">The score to populate the statistics of.</param>
|
||||||
public void PopulateMaximumStatistics(ScoreInfo score) => scoreImporter.PopulateMaximumStatistics(score);
|
public void PopulateMaximumStatistics(ScoreInfo score)
|
||||||
|
{
|
||||||
|
Debug.Assert(score.BeatmapInfo != null);
|
||||||
|
LegacyScoreDecoder.PopulateMaximumStatistics(score, beatmaps().GetWorkingBeatmap(score.BeatmapInfo.Detach()));
|
||||||
|
}
|
||||||
|
|
||||||
#region Implementation of IPresentImports<ScoreInfo>
|
#region Implementation of IPresentImports<ScoreInfo>
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ using osu.Game.Rulesets.Scoring;
|
|||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
||||||
{
|
{
|
||||||
@ -111,6 +112,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
|||||||
private readonly double accuracyD;
|
private readonly double accuracyD;
|
||||||
private readonly bool withFlair;
|
private readonly bool withFlair;
|
||||||
|
|
||||||
|
private readonly bool isFailedSDueToMisses;
|
||||||
|
private RankText failedSRankText;
|
||||||
|
|
||||||
public AccuracyCircle(ScoreInfo score, bool withFlair = false)
|
public AccuracyCircle(ScoreInfo score, bool withFlair = false)
|
||||||
{
|
{
|
||||||
this.score = score;
|
this.score = score;
|
||||||
@ -119,10 +123,13 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
|||||||
ScoreProcessor scoreProcessor = score.Ruleset.CreateInstance().CreateScoreProcessor();
|
ScoreProcessor scoreProcessor = score.Ruleset.CreateInstance().CreateScoreProcessor();
|
||||||
accuracyX = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.X);
|
accuracyX = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.X);
|
||||||
accuracyS = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.S);
|
accuracyS = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.S);
|
||||||
|
|
||||||
accuracyA = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.A);
|
accuracyA = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.A);
|
||||||
accuracyB = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.B);
|
accuracyB = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.B);
|
||||||
accuracyC = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.C);
|
accuracyC = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.C);
|
||||||
accuracyD = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.D);
|
accuracyD = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.D);
|
||||||
|
|
||||||
|
isFailedSDueToMisses = score.Accuracy >= accuracyS && score.Rank == ScoreRank.A;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -249,6 +256,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
|||||||
|
|
||||||
if (withFlair)
|
if (withFlair)
|
||||||
{
|
{
|
||||||
|
if (isFailedSDueToMisses)
|
||||||
|
AddInternal(failedSRankText = new RankText(ScoreRank.S));
|
||||||
|
|
||||||
AddRangeInternal(new Drawable[]
|
AddRangeInternal(new Drawable[]
|
||||||
{
|
{
|
||||||
rankImpactSound = new PoolableSkinnableSample(new SampleInfo(impactSampleName)),
|
rankImpactSound = new PoolableSkinnableSample(new SampleInfo(impactSampleName)),
|
||||||
@ -387,6 +397,31 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFailedSDueToMisses)
|
||||||
|
{
|
||||||
|
const double adjust_duration = 200;
|
||||||
|
|
||||||
|
using (BeginDelayedSequence(TEXT_APPEAR_DELAY - adjust_duration))
|
||||||
|
{
|
||||||
|
failedSRankText.FadeIn(adjust_duration);
|
||||||
|
|
||||||
|
using (BeginDelayedSequence(adjust_duration))
|
||||||
|
{
|
||||||
|
failedSRankText
|
||||||
|
.FadeColour(Color4.Red, 800, Easing.Out)
|
||||||
|
.RotateTo(10, 1000, Easing.Out)
|
||||||
|
.MoveToY(100, 1000, Easing.In)
|
||||||
|
.FadeOut(800, Easing.Out);
|
||||||
|
|
||||||
|
accuracyCircle
|
||||||
|
.FillTo(accuracyS - NOTCH_WIDTH_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint);
|
||||||
|
|
||||||
|
badges.Single(b => b.Rank == ScoreRank.S)
|
||||||
|
.FadeOut(70, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly double displayPosition;
|
private readonly double displayPosition;
|
||||||
|
|
||||||
private readonly ScoreRank rank;
|
public readonly ScoreRank Rank;
|
||||||
|
|
||||||
private Drawable rankContainer;
|
private Drawable rankContainer;
|
||||||
private Drawable overlay;
|
private Drawable overlay;
|
||||||
@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
|||||||
{
|
{
|
||||||
Accuracy = accuracy;
|
Accuracy = accuracy;
|
||||||
displayPosition = position;
|
displayPosition = position;
|
||||||
this.rank = rank;
|
Rank = rank;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
Alpha = 0;
|
Alpha = 0;
|
||||||
@ -62,7 +62,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
|||||||
Size = new Vector2(28, 14),
|
Size = new Vector2(28, 14),
|
||||||
Children = new[]
|
Children = new[]
|
||||||
{
|
{
|
||||||
new DrawableRank(rank),
|
new DrawableRank(Rank),
|
||||||
overlay = new CircularContainer
|
overlay = new CircularContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
@ -71,7 +71,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
|
|||||||
EdgeEffect = new EdgeEffectParameters
|
EdgeEffect = new EdgeEffectParameters
|
||||||
{
|
{
|
||||||
Type = EdgeEffectType.Glow,
|
Type = EdgeEffectType.Glow,
|
||||||
Colour = OsuColour.ForRank(rank).Opacity(0.2f),
|
Colour = OsuColour.ForRank(Rank).Opacity(0.2f),
|
||||||
Radius = 10,
|
Radius = 10,
|
||||||
},
|
},
|
||||||
Child = new Box
|
Child = new Box
|
||||||
|
Loading…
Reference in New Issue
Block a user