1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 23:12:56 +08:00

Merge pull request #23638 from smoogipoo/scorev2

Replace lazer scoring with "ScoreV2"
This commit is contained in:
Dean Herbert 2023-05-30 15:29:48 +09:00 committed by GitHub
commit a436f858df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 544 additions and 642 deletions

View File

@ -1,17 +1,30 @@
// 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;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Scoring namespace osu.Game.Rulesets.Catch.Scoring
{ {
public partial class CatchScoreProcessor : ScoreProcessor public partial class CatchScoreProcessor : ScoreProcessor
{ {
private const int combo_cap = 200;
private const double combo_base = 4;
public CatchScoreProcessor() public CatchScoreProcessor()
: base(new CatchRuleset()) : base(new CatchRuleset())
{ {
} }
protected override double ClassicScoreMultiplier => 28; protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
return 600000 * comboProgress
+ 400000 * Accuracy.Value * accuracyProgress
+ bonusPortion;
}
protected override double GetComboScoreChange(JudgementResult result)
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
} }
} }

View File

@ -1,23 +1,29 @@
// 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 osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Scoring namespace osu.Game.Rulesets.Mania.Scoring
{ {
internal partial class ManiaScoreProcessor : ScoreProcessor public partial class ManiaScoreProcessor : ScoreProcessor
{ {
private const double combo_base = 4;
public ManiaScoreProcessor() public ManiaScoreProcessor()
: base(new ManiaRuleset()) : base(new ManiaRuleset())
{ {
} }
protected override double DefaultAccuracyPortion => 0.99; protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
return 200000 * comboProgress
+ 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
+ bonusPortion;
}
protected override double DefaultComboPortion => 0.01; protected override double GetComboScoreChange(JudgementResult result)
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
protected override double ClassicScoreMultiplier => 16;
} }
} }

View File

@ -1,12 +1,7 @@
// 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 osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring namespace osu.Game.Rulesets.Osu.Scoring
@ -18,21 +13,11 @@ namespace osu.Game.Rulesets.Osu.Scoring
{ {
} }
protected override double ClassicScoreMultiplier => 36; protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
protected override HitEvent CreateHitEvent(JudgementResult result)
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement)
{ {
switch (hitObject) return 700000 * comboProgress
{ + 300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress
case HitCircle: + bonusPortion;
return new OsuHitCircleJudgementResult(hitObject, judgement);
default:
return new OsuJudgementResult(hitObject, judgement);
}
} }
} }
} }

View File

@ -91,8 +91,9 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{ {
prepareDrawableRulesetAndBeatmap(false); prepareDrawableRulesetAndBeatmap(false);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); var hit = new Hit();
assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(hit, new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(hit), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle);
} }
[Test] [Test]

View File

@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit(this)
{ {
StartTime = startTime, StartTime = startTime,
Samples = Samples Samples = Samples
@ -117,6 +117,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
{ {
// The strong hit of the drum roll doesn't actually provide any score. // The strong hit of the drum roll doesn't actually provide any score.
public override Judgement CreateJudgement() => new IgnoreJudgement(); public override Judgement CreateJudgement() => new IgnoreJudgement();
public StrongNestedHit(TaikoHitObject parent)
: base(parent)
{
}
} }
#region LegacyBeatmapEncoder #region LegacyBeatmapEncoder

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
public override double MaximumJudgementOffset => HitWindow; public override double MaximumJudgementOffset => HitWindow;
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit(this)
{ {
StartTime = startTime, StartTime = startTime,
Samples = Samples Samples = Samples
@ -41,6 +41,10 @@ namespace osu.Game.Rulesets.Taiko.Objects
public class StrongNestedHit : StrongNestedHitObject public class StrongNestedHit : StrongNestedHitObject
{ {
public StrongNestedHit(TaikoHitObject parent)
: base(parent)
{
}
} }
} }
} }

View File

@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
} }
} }
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit(this)
{ {
StartTime = startTime, StartTime = startTime,
Samples = Samples Samples = Samples
@ -80,6 +80,10 @@ namespace osu.Game.Rulesets.Taiko.Objects
public class StrongNestedHit : StrongNestedHitObject public class StrongNestedHit : StrongNestedHitObject
{ {
public StrongNestedHit(TaikoHitObject parent)
: base(parent)
{
}
} }
} }
} }

View File

@ -15,6 +15,13 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// </summary> /// </summary>
public abstract class StrongNestedHitObject : TaikoHitObject public abstract class StrongNestedHitObject : TaikoHitObject
{ {
public readonly TaikoHitObject Parent;
protected StrongNestedHitObject(TaikoHitObject parent)
{
Parent = parent;
}
public override Judgement CreateJudgement() => new TaikoStrongJudgement(); public override Judgement CreateJudgement() => new TaikoStrongJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;

View File

@ -1,23 +1,44 @@
// 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 osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Scoring namespace osu.Game.Rulesets.Taiko.Scoring
{ {
internal partial class TaikoScoreProcessor : ScoreProcessor public partial class TaikoScoreProcessor : ScoreProcessor
{ {
private const double combo_base = 4;
public TaikoScoreProcessor() public TaikoScoreProcessor()
: base(new TaikoRuleset()) : base(new TaikoRuleset())
{ {
} }
protected override double DefaultAccuracyPortion => 0.75; protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
return 250000 * comboProgress
+ 750000 * Math.Pow(Accuracy.Value, 3.6) * accuracyProgress
+ bonusPortion;
}
protected override double DefaultComboPortion => 0.25; protected override double GetBonusScoreChange(JudgementResult result) => base.GetBonusScoreChange(result) * strongScaleValue(result);
protected override double ClassicScoreMultiplier => 22; protected override double GetComboScoreChange(JudgementResult result)
{
return Judgement.ToNumericResult(result.Type)
* Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base))
* strongScaleValue(result);
}
private double strongScaleValue(JudgementResult result)
{
if (result.HitObject is StrongNestedHitObject strong)
return strong.Parent is DrumRollTick ? 3 : 7;
return 1;
}
} }
} }

View File

@ -76,22 +76,38 @@ namespace osu.Game.Tests.Gameplay
// Reset with a miss instead. // Reset with a miss instead.
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
{ {
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int> { { HitResult.Miss, 1 } }, DateTimeOffset.Now) Header = new FrameHeader(0, 0, 0, 0, new Dictionary<HitResult, int> { { HitResult.Miss, 1 } }, new ScoreProcessorStatistics
{
MaximumBaseScore = 300,
BaseScore = 0,
AccuracyJudgementCount = 1,
ComboPortion = 0,
BonusPortion = 0
}, DateTimeOffset.Now)
}); });
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero); Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0)); Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(0));
// Reset with no judged hit. // Reset with no judged hit.
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
{ {
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int>(), DateTimeOffset.Now) Header = new FrameHeader(0, 0, 0, 0, new Dictionary<HitResult, int>(), new ScoreProcessorStatistics
{
MaximumBaseScore = 0,
BaseScore = 0,
AccuracyJudgementCount = 0,
ComboPortion = 0,
BonusPortion = 0
}, DateTimeOffset.Now)
}); });
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero); Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
Assert.That(scoreProcessor.JudgedHits, Is.Zero); Assert.That(scoreProcessor.JudgedHits, Is.Zero);
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0)); Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
} }
[Test] [Test]

View File

@ -179,7 +179,7 @@ namespace osu.Game.Tests.Resources
BeatmapHash = beatmap.Hash, BeatmapHash = beatmap.Hash,
Ruleset = beatmap.Ruleset, Ruleset = beatmap.Ruleset,
Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() }, Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() },
TotalScore = 2845370, TotalScore = 284537,
Accuracy = 0.95, Accuracy = 0.95,
MaxCombo = 999, MaxCombo = 999,
Position = 1, Position = 1,

View File

@ -14,11 +14,12 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Rulesets.Scoring namespace osu.Game.Tests.Rulesets.Scoring
@ -31,7 +32,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
scoreProcessor = new ScoreProcessor(new TestRuleset()); scoreProcessor = new ScoreProcessor(new OsuRuleset());
beatmap = new TestBeatmap(new RulesetInfo()) beatmap = new TestBeatmap(new RulesetInfo())
{ {
HitObjects = new List<HitObject> HitObjects = new List<HitObject>
@ -41,15 +42,14 @@ namespace osu.Game.Tests.Rulesets.Scoring
}; };
} }
[TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)] [TestCase(ScoringMode.Standardised, HitResult.Meh, 116_667)]
[TestCase(ScoringMode.Standardised, HitResult.Ok, 800_000)] [TestCase(ScoringMode.Standardised, HitResult.Ok, 233_338)]
[TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)] [TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
[TestCase(ScoringMode.Classic, HitResult.Meh, 20)] [TestCase(ScoringMode.Classic, HitResult.Meh, 0)]
[TestCase(ScoringMode.Classic, HitResult.Ok, 23)] [TestCase(ScoringMode.Classic, HitResult.Ok, 2)]
[TestCase(ScoringMode.Classic, HitResult.Great, 36)] [TestCase(ScoringMode.Classic, HitResult.Great, 36)]
public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore) public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{ {
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(beatmap); scoreProcessor.ApplyBeatmap(beatmap);
var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new OsuJudgement()) var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new OsuJudgement())
@ -58,7 +58,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
}; };
scoreProcessor.ApplyResult(judgementResult); scoreProcessor.ApplyResult(judgementResult);
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(expectedScore).Within(0.5d)); Assert.That(scoreProcessor.GetDisplayScore(scoringMode), Is.EqualTo(expectedScore).Within(0.5d));
} }
/// <summary> /// <summary>
@ -70,39 +70,29 @@ namespace osu.Game.Tests.Rulesets.Scoring
/// <param name="expectedScore">Expected score after all objects have been judged, rounded to the nearest integer.</param> /// <param name="expectedScore">Expected score after all objects have been judged, rounded to the nearest integer.</param>
/// <remarks> /// <remarks>
/// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo. /// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo.
/// <para>
/// For standardised scoring, <paramref name="expectedScore"/> is calculated using the following formula:
/// 1_000_000 * (((3 * <paramref name="hitResult"/>) / (4 * <paramref name="maxResult"/>)) * 30% + (bestCombo / maxCombo) * 70%)
/// </para>
/// <para>
/// For classic scoring, <paramref name="expectedScore"/> is calculated using the following formula:
/// <paramref name="hitResult"/> / <paramref name="maxResult"/> * 936
/// where 936 is simplified from:
/// 75% * 4 * 300 * (1 + 1/25)
/// </para>
/// </remarks> /// </remarks>
[TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] // (3 * 0) / (4 * 300) * 300_000 + (0 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)]
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] // (3 * 50) / (4 * 300) * 300_000 + (2 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 79_333)]
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] // (3 * 100) / (4 * 300) * 300_000 + (2 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 158_667)]
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 492_857)] // (3 * 200) / (4 * 350) * 300_000 + (2 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 302_402)]
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] // (3 * 300) / (4 * 300) * 300_000 + (2 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 492_894)]
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] // (3 * 350) / (4 * 350) * 300_000 + (2 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 492_894)]
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] // (3 * 0) / (4 * 10) * 300_000 + 700_000 (max combo 0) [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 925_000)] // (3 * 10) / (4 * 10) * 300_000 + 700_000 (max combo 0) [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 541_894)]
[TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (3 * 0) / (4 * 30) * 300_000 + (0 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] // (3 * 30) / (4 * 30) * 300_000 + (0 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 492_894)]
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points) [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)]
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points) [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)]
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 86)] [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 4)]
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 104)] [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15)]
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 140)] [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 53)]
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 190)] [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 140)]
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 190)] [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 140)]
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 18)] [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 31)] [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 11)]
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 12)] [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 9)]
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 36)] [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 36)]
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 36)] [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 36)]
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore) public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
@ -113,59 +103,18 @@ namespace osu.Game.Tests.Rulesets.Scoring
{ {
HitObjects = new List<HitObject>(Enumerable.Repeat(new TestHitObject(maxResult), 4)) HitObjects = new List<HitObject>(Enumerable.Repeat(new TestHitObject(maxResult), 4))
}; };
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(fourObjectBeatmap); scoreProcessor.ApplyBeatmap(fourObjectBeatmap);
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new Judgement()) var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new TestJudgement(maxResult))
{ {
Type = i == 2 ? minResult : hitResult Type = i == 2 ? minResult : hitResult
}; };
scoreProcessor.ApplyResult(judgementResult); scoreProcessor.ApplyResult(judgementResult);
} }
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(expectedScore).Within(0.5d)); Assert.That(scoreProcessor.GetDisplayScore(scoringMode), Is.EqualTo(expectedScore).Within(0.5d));
}
/// <remarks>
/// This test uses a beatmap with four small ticks and one object with the <see cref="Judgement.MaxResult"/> of <see cref="HitResult.Ok"/>.
/// Its goal is to ensure that with the <see cref="ScoringMode"/> of <see cref="ScoringMode.Standardised"/>,
/// small ticks contribute to the accuracy portion, but not the combo portion.
/// In contrast, <see cref="ScoringMode.Classic"/> does not have separate combo and accuracy portion (they are multiplied by each other).
/// </remarks>
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] // (3 * 10 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] // (3 * 0 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 34)]
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 30)]
public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{
IEnumerable<HitObject> hitObjects = Enumerable
.Repeat(new TestHitObject(HitResult.SmallTickHit), 4)
.Append(new TestHitObject(HitResult.Ok));
IBeatmap fiveObjectBeatmap = new TestBeatmap(new RulesetInfo())
{
HitObjects = hitObjects.ToList()
};
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(fiveObjectBeatmap);
for (int i = 0; i < 4; i++)
{
var judgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects[i], new Judgement())
{
Type = i == 2 ? HitResult.SmallTickMiss : hitResult
};
scoreProcessor.ApplyResult(judgementResult);
}
var lastJudgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects.Last(), new Judgement())
{
Type = HitResult.Ok
};
scoreProcessor.ApplyResult(lastJudgementResult);
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(expectedScore).Within(0.5d));
} }
[Test] [Test]
@ -173,10 +122,9 @@ namespace osu.Game.Tests.Rulesets.Scoring
[Values(ScoringMode.Standardised, ScoringMode.Classic)] [Values(ScoringMode.Standardised, ScoringMode.Classic)]
ScoringMode scoringMode) ScoringMode scoringMode)
{ {
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(new TestBeatmap(new RulesetInfo())); scoreProcessor.ApplyBeatmap(new TestBeatmap(new RulesetInfo()));
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero); Assert.That(scoreProcessor.GetDisplayScore(scoringMode), Is.Zero);
} }
[TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)] [TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)]
@ -294,28 +242,6 @@ namespace osu.Game.Tests.Rulesets.Scoring
Assert.AreEqual(expectedReturnValue, hitResult.IsScorable()); Assert.AreEqual(expectedReturnValue, hitResult.IsScorable());
} }
[TestCase(HitResult.Perfect, 1_000_000)]
[TestCase(HitResult.SmallTickHit, 1_000_000)]
[TestCase(HitResult.LargeTickHit, 1_000_000)]
[TestCase(HitResult.SmallBonus, 1_000_000 + Judgement.SMALL_BONUS_SCORE)]
[TestCase(HitResult.LargeBonus, 1_000_000 + Judgement.LARGE_BONUS_SCORE)]
public void TestGetScoreWithExternalStatistics(HitResult result, int expectedScore)
{
var statistic = new Dictionary<HitResult, int> { { result, 1 } };
scoreProcessor.ApplyBeatmap(new Beatmap
{
HitObjects = { new TestHitObject(result) }
});
Assert.That(scoreProcessor.ComputeScore(ScoringMode.Standardised, new ScoreInfo
{
Ruleset = new TestRuleset().RulesetInfo,
MaxCombo = result.AffectsCombo() ? 1 : 0,
Statistics = statistic
}), Is.EqualTo(expectedScore).Within(0.5d));
}
#pragma warning disable CS0618 #pragma warning disable CS0618
[Test] [Test]
public void TestLegacyComboIncrease() public void TestLegacyComboIncrease()
@ -330,29 +256,6 @@ namespace osu.Game.Tests.Rulesets.Scoring
Assert.That(HitResult.LegacyComboIncrease.IsHit(), Is.True); Assert.That(HitResult.LegacyComboIncrease.IsHit(), Is.True);
Assert.That(HitResult.LegacyComboIncrease.IsScorable(), Is.True); Assert.That(HitResult.LegacyComboIncrease.IsScorable(), Is.True);
Assert.That(HitResultExtensions.ALL_TYPES, Does.Not.Contain(HitResult.LegacyComboIncrease)); Assert.That(HitResultExtensions.ALL_TYPES, Does.Not.Contain(HitResult.LegacyComboIncrease));
// Cannot be used to apply results.
Assert.Throws<ArgumentException>(() => scoreProcessor.ApplyBeatmap(new Beatmap
{
HitObjects = { new TestHitObject(HitResult.LegacyComboIncrease) }
}));
ScoreInfo testScore = new ScoreInfo
{
MaxCombo = 1,
Statistics = new Dictionary<HitResult, int>
{
{ HitResult.Great, 1 }
},
MaximumStatistics = new Dictionary<HitResult, int>
{
{ HitResult.Great, 1 },
{ HitResult.LegacyComboIncrease, 1 }
}
};
double totalScore = new TestScoreProcessor().ComputeScore(ScoringMode.Standardised, testScore);
Assert.That(totalScore, Is.EqualTo(750_000)); // 500K from accuracy (100%), and 250K from combo (50%).
} }
#pragma warning restore CS0618 #pragma warning restore CS0618
@ -362,36 +265,30 @@ namespace osu.Game.Tests.Rulesets.Scoring
const int count_judgements = 1000; const int count_judgements = 1000;
const int count_misses = 1; const int count_misses = 1;
double actual = new TestScoreProcessor().ComputeAccuracy(new ScoreInfo beatmap = new TestBeatmap(new RulesetInfo())
{ {
Statistics = new Dictionary<HitResult, int> HitObjects = new List<HitObject>(Enumerable.Repeat(new TestHitObject(HitResult.Great), count_judgements))
};
scoreProcessor = new TestScoreProcessor();
scoreProcessor.ApplyBeatmap(beatmap);
for (int i = 0; i < beatmap.HitObjects.Count; i++)
{ {
{ HitResult.Great, count_judgements - count_misses }, scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], new TestJudgement(HitResult.Great))
{ HitResult.Miss, count_misses } {
} Type = i == 0 ? HitResult.Miss : HitResult.Great
}); });
}
const double expected = (count_judgements - count_misses) / (double)count_judgements; const double expected = (count_judgements - count_misses) / (double)count_judgements;
double actual = scoreProcessor.Accuracy.Value;
Assert.That(actual, Is.Not.EqualTo(0.0)); Assert.That(actual, Is.Not.EqualTo(0.0));
Assert.That(actual, Is.Not.EqualTo(1.0)); Assert.That(actual, Is.Not.EqualTo(1.0));
Assert.That(actual, Is.EqualTo(expected).Within(Precision.FLOAT_EPSILON)); Assert.That(actual, Is.EqualTo(expected).Within(Precision.FLOAT_EPSILON));
} }
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new NotImplementedException();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
public override string Description => string.Empty;
public override string ShortName => string.Empty;
}
private class TestJudgement : Judgement private class TestJudgement : Judgement
{ {
public override HitResult MaxResult { get; } public override HitResult MaxResult { get; }
@ -419,14 +316,18 @@ namespace osu.Game.Tests.Rulesets.Scoring
private partial class TestScoreProcessor : ScoreProcessor private partial class TestScoreProcessor : ScoreProcessor
{ {
protected override double DefaultAccuracyPortion => 0.5;
protected override double DefaultComboPortion => 0.5;
public TestScoreProcessor() public TestScoreProcessor()
: base(new TestRuleset()) : base(new TestRuleset())
{ {
} }
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
return 500000 * comboProgress +
500000 * Accuracy.Value * accuracyProgress +
bonusPortion;
}
// ReSharper disable once MemberHidesStaticFromOuterClass // ReSharper disable once MemberHidesStaticFromOuterClass
private class TestRuleset : Ruleset private class TestRuleset : Ruleset
{ {

View File

@ -31,8 +31,8 @@ namespace osu.Game.Tests.Visual.Gameplay
private HUDOverlay hudOverlay = null!; private HUDOverlay hudOverlay = null!;
[Cached] [Cached(typeof(ScoreProcessor))]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
[Cached(typeof(HealthProcessor))] [Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);

View File

@ -22,6 +22,7 @@ using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring.Legacy;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input; using osuTK.Input;
@ -124,8 +125,8 @@ namespace osu.Game.Tests.Visual.Gameplay
graphs.Clear(); graphs.Clear();
legend.Clear(); legend.Clear();
runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Standardised } }); runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()), ScoringMode.Standardised);
runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Classic } }); runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()), ScoringMode.Classic);
runScoreV1(); runScoreV1();
runScoreV2(); runScoreV2();
@ -218,7 +219,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
} }
private void runForProcessor(string name, Color4 colour, ScoreProcessor processor) private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode)
{ {
int maxCombo = sliderMaxCombo.Current.Value; int maxCombo = sliderMaxCombo.Current.Value;
@ -232,10 +233,10 @@ namespace osu.Game.Tests.Visual.Gameplay
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }), () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }),
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }), () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }), () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }),
() => (int)processor.TotalScore.Value); () => processor.GetDisplayScore(mode));
} }
private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func<int> getTotalScore) private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func<long> getTotalScore)
{ {
int maxCombo = sliderMaxCombo.Current.Value; int maxCombo = sliderMaxCombo.Current.Value;

View File

@ -22,8 +22,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneSkinEditorMultipleSkins : SkinnableTestScene public partial class TestSceneSkinEditorMultipleSkins : SkinnableTestScene
{ {
[Cached] [Cached(typeof(ScoreProcessor))]
private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
[Cached(typeof(HealthProcessor))] [Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);

View File

@ -28,8 +28,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private HUDOverlay hudOverlay; private HUDOverlay hudOverlay;
[Cached] [Cached(typeof(ScoreProcessor))]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
[Cached(typeof(HealthProcessor))] [Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);

View File

@ -10,13 +10,14 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Tests.Gameplay;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneSkinnableScoreCounter : SkinnableHUDComponentTestScene public partial class TestSceneSkinnableScoreCounter : SkinnableHUDComponentTestScene
{ {
[Cached] [Cached(typeof(ScoreProcessor))]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); private ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor;
protected override Drawable CreateDefaultImplementation() => new DefaultScoreCounter(); protected override Drawable CreateDefaultImplementation() => new DefaultScoreCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyScoreCounter(); protected override Drawable CreateLegacyImplementation() => new LegacyScoreCounter();

View File

@ -16,13 +16,14 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Tests.Gameplay;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneSoloGameplayLeaderboard : OsuTestScene public partial class TestSceneSoloGameplayLeaderboard : OsuTestScene
{ {
[Cached] [Cached(typeof(ScoreProcessor))]
private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); private readonly ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor;
private readonly BindableList<ScoreInfo> scores = new BindableList<ScoreInfo>(); private readonly BindableList<ScoreInfo> scores = new BindableList<ScoreInfo>();

View File

@ -21,7 +21,6 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
@ -188,15 +187,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
if (!lastHeaders.TryGetValue(userId, out var header)) if (!lastHeaders.TryGetValue(userId, out var header))
{ {
lastHeaders[userId] = header = new FrameHeader(new ScoreInfo lastHeaders[userId] = header = new FrameHeader(0, 0, 0, 0, new Dictionary<HitResult, int>
{
Statistics = new Dictionary<HitResult, int>
{ {
[HitResult.Miss] = 0, [HitResult.Miss] = 0,
[HitResult.Meh] = 0, [HitResult.Meh] = 0,
[HitResult.Great] = 0 [HitResult.Great] = 0
} }, new ScoreProcessorStatistics(), DateTimeOffset.Now);
});
} }
switch (RNG.Next(0, 3)) switch (RNG.Next(0, 3))

View File

@ -1,7 +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.
using System.Diagnostics;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -12,7 +11,6 @@ using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Carousel;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
@ -143,25 +141,20 @@ namespace osu.Game.Tests.Visual.SongSelect
testScoreInfo.User = API.LocalUser.Value; testScoreInfo.User = API.LocalUser.Value;
testScoreInfo.Rank = ScoreRank.B; testScoreInfo.Rank = ScoreRank.B;
testScoreInfo.TotalScore = scoreManager.GetTotalScore(testScoreInfo, ScoringMode.Classic);
scoreManager.Import(testScoreInfo); scoreManager.Import(testScoreInfo);
}); });
AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.B); AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.B);
AddStep("Add higher score for current user", () => AddStep("Add higher-graded score for current user", () =>
{ {
var testScoreInfo2 = TestResources.CreateTestScoreInfo(importedBeatmap); var testScoreInfo2 = TestResources.CreateTestScoreInfo(importedBeatmap);
testScoreInfo2.User = API.LocalUser.Value; testScoreInfo2.User = API.LocalUser.Value;
testScoreInfo2.Rank = ScoreRank.X; testScoreInfo2.Rank = ScoreRank.X;
testScoreInfo2.Statistics = testScoreInfo2.MaximumStatistics; testScoreInfo2.Statistics = testScoreInfo2.MaximumStatistics;
testScoreInfo2.TotalScore = scoreManager.GetTotalScore(testScoreInfo2); testScoreInfo2.TotalScore = testScoreInfo.TotalScore + 1;
// ensure second score has a total score (standardised) less than first one (classic)
// despite having better statistics, otherwise this test is pointless.
Debug.Assert(testScoreInfo2.TotalScore < testScoreInfo.TotalScore);
scoreManager.Import(testScoreInfo2); scoreManager.Import(testScoreInfo2);
}); });

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using MessagePack; using MessagePack;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Online.Spectator namespace osu.Game.Online.Spectator
@ -20,10 +21,10 @@ namespace osu.Game.Online.Spectator
[Key(1)] [Key(1)]
public IList<LegacyReplayFrame> Frames { get; set; } public IList<LegacyReplayFrame> Frames { get; set; }
public FrameDataBundle(ScoreInfo score, IList<LegacyReplayFrame> frames) public FrameDataBundle(ScoreInfo score, ScoreProcessor scoreProcessor, IList<LegacyReplayFrame> frames)
{ {
Frames = frames; Frames = frames;
Header = new FrameHeader(score); Header = new FrameHeader(score, scoreProcessor.GetScoreProcessorStatistics());
} }
[JsonConstructor] [JsonConstructor]

View File

@ -15,57 +15,74 @@ namespace osu.Game.Online.Spectator
public class FrameHeader public class FrameHeader
{ {
/// <summary> /// <summary>
/// The current accuracy of the score. /// The total score.
/// </summary> /// </summary>
[Key(0)] [Key(0)]
public long TotalScore { get; set; }
/// <summary>
/// The current accuracy of the score.
/// </summary>
[Key(1)]
public double Accuracy { get; set; } public double Accuracy { get; set; }
/// <summary> /// <summary>
/// The current combo of the score. /// The current combo of the score.
/// </summary> /// </summary>
[Key(1)] [Key(2)]
public int Combo { get; set; } public int Combo { get; set; }
/// <summary> /// <summary>
/// The maximum combo achieved up to the current point in time. /// The maximum combo achieved up to the current point in time.
/// </summary> /// </summary>
[Key(2)] [Key(3)]
public int MaxCombo { get; set; } public int MaxCombo { get; set; }
/// <summary> /// <summary>
/// Cumulative hit statistics. /// Cumulative hit statistics.
/// </summary> /// </summary>
[Key(3)] [Key(4)]
public Dictionary<HitResult, int> Statistics { get; set; } public Dictionary<HitResult, int> Statistics { get; set; }
/// <summary>
/// Additional statistics that guides the score processor to calculate the correct score for this frame.
/// </summary>
[Key(5)]
public ScoreProcessorStatistics ScoreProcessorStatistics { get; set; }
/// <summary> /// <summary>
/// The time at which this frame was received by the server. /// The time at which this frame was received by the server.
/// </summary> /// </summary>
[Key(4)] [Key(6)]
public DateTimeOffset ReceivedTime { get; set; } public DateTimeOffset ReceivedTime { get; set; }
/// <summary> /// <summary>
/// Construct header summary information from a point-in-time reference to a score which is actively being played. /// Construct header summary information from a point-in-time reference to a score which is actively being played.
/// </summary> /// </summary>
/// <param name="score">The score for reference.</param> /// <param name="score">The score for reference.</param>
public FrameHeader(ScoreInfo score) /// <param name="statistics">The score processor statistics for the current point in time.</param>
public FrameHeader(ScoreInfo score, ScoreProcessorStatistics statistics)
{ {
TotalScore = score.TotalScore;
Accuracy = score.Accuracy;
Combo = score.Combo; Combo = score.Combo;
MaxCombo = score.MaxCombo; MaxCombo = score.MaxCombo;
Accuracy = score.Accuracy;
// copy for safety // copy for safety
Statistics = new Dictionary<HitResult, int>(score.Statistics); Statistics = new Dictionary<HitResult, int>(score.Statistics);
ScoreProcessorStatistics = statistics;
} }
[JsonConstructor] [JsonConstructor]
[SerializationConstructor] [SerializationConstructor]
public FrameHeader(double accuracy, int combo, int maxCombo, Dictionary<HitResult, int> statistics, DateTimeOffset receivedTime) public FrameHeader(long totalScore, double accuracy, int combo, int maxCombo, Dictionary<HitResult, int> statistics, ScoreProcessorStatistics scoreProcessorStatistics, DateTimeOffset receivedTime)
{ {
TotalScore = totalScore;
Accuracy = accuracy;
Combo = combo; Combo = combo;
MaxCombo = maxCombo; MaxCombo = maxCombo;
Accuracy = accuracy;
Statistics = statistics; Statistics = statistics;
ScoreProcessorStatistics = scoreProcessorStatistics;
ReceivedTime = receivedTime; ReceivedTime = receivedTime;
} }
} }

View File

@ -16,6 +16,7 @@ using osu.Game.Online.API;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -82,6 +83,7 @@ namespace osu.Game.Online.Spectator
private IBeatmap? currentBeatmap; private IBeatmap? currentBeatmap;
private Score? currentScore; private Score? currentScore;
private long? currentScoreToken; private long? currentScoreToken;
private ScoreProcessor? currentScoreProcessor;
private readonly Queue<FrameDataBundle> pendingFrameBundles = new Queue<FrameDataBundle>(); private readonly Queue<FrameDataBundle> pendingFrameBundles = new Queue<FrameDataBundle>();
@ -192,6 +194,7 @@ namespace osu.Game.Online.Spectator
currentBeatmap = state.Beatmap; currentBeatmap = state.Beatmap;
currentScore = score; currentScore = score;
currentScoreToken = scoreToken; currentScoreToken = scoreToken;
currentScoreProcessor = state.ScoreProcessor;
BeginPlayingInternal(currentScoreToken, currentState); BeginPlayingInternal(currentScoreToken, currentState);
}); });
@ -302,9 +305,10 @@ namespace osu.Game.Online.Spectator
return; return;
Debug.Assert(currentScore != null); Debug.Assert(currentScore != null);
Debug.Assert(currentScoreProcessor != null);
var frames = pendingFrames.ToArray(); var frames = pendingFrames.ToArray();
var bundle = new FrameDataBundle(currentScore.ScoreInfo, frames); var bundle = new FrameDataBundle(currentScore.ScoreInfo, currentScoreProcessor, frames);
pendingFrames.Clear(); pendingFrames.Clear();
lastPurgeTime = Time.Current; lastPurgeTime = Time.Current;

View File

@ -14,6 +14,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
namespace osu.Game.Online.Spectator namespace osu.Game.Online.Spectator
{ {
@ -46,7 +47,9 @@ namespace osu.Game.Online.Spectator
/// <summary> /// <summary>
/// The applied <see cref="Mod"/>s. /// The applied <see cref="Mod"/>s.
/// </summary> /// </summary>
public IReadOnlyList<Mod> Mods => scoreProcessor?.Mods.Value ?? Array.Empty<Mod>(); public IReadOnlyList<Mod> Mods => scoreInfo?.Mods ?? Array.Empty<Mod>();
public Func<ScoringMode, long> GetDisplayScore => mode => scoreInfo?.GetDisplayScore(mode) ?? 0;
private IClock? referenceClock; private IClock? referenceClock;
@ -70,7 +73,6 @@ namespace osu.Game.Online.Spectator
private readonly int userId; private readonly int userId;
private SpectatorState? spectatorState; private SpectatorState? spectatorState;
private ScoreProcessor? scoreProcessor;
private ScoreInfo? scoreInfo; private ScoreInfo? scoreInfo;
public SpectatorScoreProcessor(int userId) public SpectatorScoreProcessor(int userId)
@ -94,19 +96,15 @@ namespace osu.Game.Online.Spectator
{ {
if (!spectatorStates.TryGetValue(userId, out var userState) || userState.BeatmapID == null || userState.RulesetID == null) if (!spectatorStates.TryGetValue(userId, out var userState) || userState.BeatmapID == null || userState.RulesetID == null)
{ {
scoreProcessor?.RemoveAndDisposeImmediately();
scoreProcessor = null;
scoreInfo = null; scoreInfo = null;
spectatorState = null; spectatorState = null;
replayFrames.Clear(); replayFrames.Clear();
return; return;
} }
if (scoreProcessor != null) if (scoreInfo != null)
return; return;
Debug.Assert(scoreInfo == null);
RulesetInfo? rulesetInfo = rulesetStore.GetRuleset(userState.RulesetID.Value); RulesetInfo? rulesetInfo = rulesetStore.GetRuleset(userState.RulesetID.Value);
if (rulesetInfo == null) if (rulesetInfo == null)
return; return;
@ -114,9 +112,11 @@ namespace osu.Game.Online.Spectator
Ruleset ruleset = rulesetInfo.CreateInstance(); Ruleset ruleset = rulesetInfo.CreateInstance();
spectatorState = userState; spectatorState = userState;
scoreInfo = new ScoreInfo { Ruleset = rulesetInfo }; scoreInfo = new ScoreInfo
scoreProcessor = ruleset.CreateScoreProcessor(); {
scoreProcessor.Mods.Value = userState.Mods.Select(m => m.ToMod(ruleset)).ToArray(); Ruleset = rulesetInfo,
Mods = userState.Mods.Select(m => m.ToMod(ruleset)).ToArray()
};
} }
private void onNewFrames(int incomingUserId, FrameDataBundle bundle) private void onNewFrames(int incomingUserId, FrameDataBundle bundle)
@ -126,7 +126,7 @@ namespace osu.Game.Online.Spectator
Schedule(() => Schedule(() =>
{ {
if (scoreProcessor == null) if (scoreInfo == null)
return; return;
replayFrames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header)); replayFrames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
@ -140,7 +140,6 @@ namespace osu.Game.Online.Spectator
return; return;
Debug.Assert(spectatorState != null); Debug.Assert(spectatorState != null);
Debug.Assert(scoreProcessor != null);
int frameIndex = replayFrames.BinarySearch(new TimedFrame(ReferenceClock.CurrentTime)); int frameIndex = replayFrames.BinarySearch(new TimedFrame(ReferenceClock.CurrentTime));
if (frameIndex < 0) if (frameIndex < 0)
@ -150,14 +149,15 @@ namespace osu.Game.Online.Spectator
TimedFrame frame = replayFrames[frameIndex]; TimedFrame frame = replayFrames[frameIndex];
Debug.Assert(frame.Header != null); Debug.Assert(frame.Header != null);
scoreInfo.Accuracy = frame.Header.Accuracy;
scoreInfo.MaxCombo = frame.Header.MaxCombo; scoreInfo.MaxCombo = frame.Header.MaxCombo;
scoreInfo.Statistics = frame.Header.Statistics; scoreInfo.Statistics = frame.Header.Statistics;
scoreInfo.MaximumStatistics = spectatorState.MaximumStatistics; scoreInfo.MaximumStatistics = spectatorState.MaximumStatistics;
scoreInfo.TotalScore = frame.Header.TotalScore;
Accuracy.Value = frame.Header.Accuracy; Accuracy.Value = frame.Header.Accuracy;
Combo.Value = frame.Header.Combo; Combo.Value = frame.Header.Combo;
TotalScore.Value = frame.Header.TotalScore;
TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo);
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -62,11 +62,13 @@ namespace osu.Game.Rulesets.Difficulty
.GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count())) .GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count()))
.ToDictionary(pair => pair.hitResult, pair => pair.count); .ToDictionary(pair => pair.hitResult, pair => pair.count);
perfectPlay.Statistics = statistics; perfectPlay.Statistics = statistics;
perfectPlay.MaximumStatistics = statistics;
// calculate total score // calculate total score
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor(); ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = perfectPlay.Mods; scoreProcessor.Mods.Value = perfectPlay.Mods;
perfectPlay.TotalScore = scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay); scoreProcessor.ApplyBeatmap(playableBeatmap);
perfectPlay.TotalScore = scoreProcessor.MaximumTotalScore;
// compute rank achieved // compute rank achieved
// default to SS, then adjust the rank with mods // default to SS, then adjust the rank with mods

View File

@ -64,6 +64,11 @@ namespace osu.Game.Rulesets.Judgements
/// </summary> /// </summary>
public int ComboAtJudgement { get; internal set; } public int ComboAtJudgement { get; internal set; }
/// <summary>
/// The combo after this <see cref="JudgementResult"/> occurred.
/// </summary>
public int ComboAfterJudgement { get; internal set; }
/// <summary> /// <summary>
/// The highest combo achieved prior to this <see cref="JudgementResult"/> occurring. /// The highest combo achieved prior to this <see cref="JudgementResult"/> occurring.
/// </summary> /// </summary>

View File

@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Scoring
=> AffectsCombo(result) && !IsHit(result); => AffectsCombo(result) && !IsHit(result);
/// <summary> /// <summary>
/// Whether a <see cref="HitResult"/> increases/breaks the combo, and affects the combo portion of the score. /// Whether a <see cref="HitResult"/> increases or breaks the combo.
/// </summary> /// </summary>
public static bool AffectsCombo(this HitResult result) public static bool AffectsCombo(this HitResult result)
{ {

View File

@ -4,11 +4,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq; using System.Linq;
using MessagePack;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Localisation; using osu.Game.Localisation;
@ -22,6 +21,8 @@ namespace osu.Game.Rulesets.Scoring
{ {
public partial class ScoreProcessor : JudgementProcessor public partial class ScoreProcessor : JudgementProcessor
{ {
public const double MAX_SCORE = 1000000;
private const double accuracy_cutoff_x = 1; private const double accuracy_cutoff_x = 1;
private const double accuracy_cutoff_s = 0.95; private const double accuracy_cutoff_s = 0.95;
private const double accuracy_cutoff_a = 0.9; private const double accuracy_cutoff_a = 0.9;
@ -29,8 +30,6 @@ namespace osu.Game.Rulesets.Scoring
private const double accuracy_cutoff_c = 0.7; private const double accuracy_cutoff_c = 0.7;
private const double accuracy_cutoff_d = 0; private const double accuracy_cutoff_d = 0;
private const double max_score = 1000000;
/// <summary> /// <summary>
/// Invoked when this <see cref="ScoreProcessor"/> was reset from a replay frame. /// Invoked when this <see cref="ScoreProcessor"/> was reset from a replay frame.
/// </summary> /// </summary>
@ -78,39 +77,72 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
public readonly BindableInt HighestCombo = new BindableInt(); public readonly BindableInt HighestCombo = new BindableInt();
/// <summary>
/// The <see cref="ScoringMode"/> used to calculate scores.
/// </summary>
public readonly Bindable<ScoringMode> Mode = new Bindable<ScoringMode>();
/// <summary> /// <summary>
/// The <see cref="HitEvent"/>s collected during gameplay thus far. /// The <see cref="HitEvent"/>s collected during gameplay thus far.
/// Intended for use with various statistics displays. /// Intended for use with various statistics displays.
/// </summary> /// </summary>
public IReadOnlyList<HitEvent> HitEvents => hitEvents; public IReadOnlyList<HitEvent> HitEvents => hitEvents;
/// <summary>
/// The default portion of <see cref="max_score"/> awarded for hitting <see cref="HitObject"/>s accurately. Defaults to 30%.
/// </summary>
protected virtual double DefaultAccuracyPortion => 0.3;
/// <summary>
/// The default portion of <see cref="max_score"/> awarded for achieving a high combo. Default to 70%.
/// </summary>
protected virtual double DefaultComboPortion => 0.7;
/// <summary>
/// An arbitrary multiplier to scale scores in the <see cref="ScoringMode.Classic"/> scoring mode.
/// </summary>
protected virtual double ClassicScoreMultiplier => 36;
/// <summary> /// <summary>
/// The ruleset this score processor is valid for. /// The ruleset this score processor is valid for.
/// </summary> /// </summary>
public readonly Ruleset Ruleset; public readonly Ruleset Ruleset;
private readonly double accuracyPortion; /// <summary>
private readonly double comboPortion; /// The maximum achievable total score.
/// </summary>
public long MaximumTotalScore { get; private set; }
/// <summary>
/// The maximum sum of accuracy-affecting judgements at the current point in time.
/// </summary>
/// <remarks>
/// Used to compute accuracy.
/// </remarks>
private double currentMaximumBaseScore;
/// <summary>
/// The sum of all accuracy-affecting judgements at the current point in time.
/// </summary>
/// <remarks>
/// Used to compute accuracy.
/// </remarks>
private double currentBaseScore;
/// <summary>
/// The maximum sum of all accuracy-affecting judgements in the beatmap.
/// </summary>
private double maximumBaseScore;
/// <summary>
/// The count of all accuracy-affecting judgements in the beatmap.
/// </summary>
private int maximumAccuracyJudgementCount;
/// <summary>
/// The count of accuracy-affecting judgements at the current point in time.
/// </summary>
private int currentAccuracyJudgementCount;
/// <summary>
/// The maximum combo score in the beatmap.
/// </summary>
private double maximumComboPortion;
/// <summary>
/// The combo score at the current point in time.
/// </summary>
private double currentComboPortion;
/// <summary>
/// The bonus score at the current point in time.
/// </summary>
private double currentBonusPortion;
/// <summary>
/// The total score multiplier.
/// </summary>
private double scoreMultiplier = 1;
public Dictionary<HitResult, int> MaximumStatistics public Dictionary<HitResult, int> MaximumStatistics
{ {
@ -123,27 +155,6 @@ namespace osu.Game.Rulesets.Scoring
} }
} }
private ScoringValues maximumScoringValues;
/// <summary>
/// Scoring values for the current play assuming all perfect hits.
/// </summary>
/// <remarks>
/// This is only used to determine the accuracy with respect to the current point in time for an ongoing play session.
/// </remarks>
private ScoringValues currentMaximumScoringValues;
/// <summary>
/// Scoring values for the current play.
/// </summary>
private ScoringValues currentScoringValues;
/// <summary>
/// The maximum <see cref="HitResult"/> of a basic (non-tick and non-bonus) hitobject.
/// Only populated via <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoreInfo)"/> or <see cref="ResetFromReplayFrame"/>.
/// </summary>
private HitResult? maxBasicResult;
private bool beatmapApplied; private bool beatmapApplied;
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>(); private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
@ -152,18 +163,10 @@ namespace osu.Game.Rulesets.Scoring
private readonly List<HitEvent> hitEvents = new List<HitEvent>(); private readonly List<HitEvent> hitEvents = new List<HitEvent>();
private HitObject? lastHitObject; private HitObject? lastHitObject;
private double scoreMultiplier = 1;
public ScoreProcessor(Ruleset ruleset) public ScoreProcessor(Ruleset ruleset)
{ {
Ruleset = ruleset; Ruleset = ruleset;
accuracyPortion = DefaultAccuracyPortion;
comboPortion = DefaultComboPortion;
if (!Precision.AlmostEquals(1.0, accuracyPortion + comboPortion))
throw new InvalidOperationException($"{nameof(DefaultAccuracyPortion)} + {nameof(DefaultComboPortion)} must equal 1.");
Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue); Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue);
Accuracy.ValueChanged += accuracy => Accuracy.ValueChanged += accuracy =>
{ {
@ -172,7 +175,6 @@ namespace osu.Game.Rulesets.Scoring
Rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue); Rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue);
}; };
Mode.ValueChanged += _ => updateScore();
Mods.ValueChanged += mods => Mods.ValueChanged += mods =>
{ {
scoreMultiplier = 1; scoreMultiplier = 1;
@ -200,10 +202,6 @@ namespace osu.Game.Rulesets.Scoring
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1; scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1;
// Always update the maximum scoring values.
applyResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
currentMaximumScoringValues.MaxCombo += result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
if (!result.Type.IsScorable()) if (!result.Type.IsScorable())
return; return;
@ -212,8 +210,21 @@ namespace osu.Game.Rulesets.Scoring
else if (result.Type.BreaksCombo()) else if (result.Type.BreaksCombo())
Combo.Value = 0; Combo.Value = 0;
applyResult(result.Type, ref currentScoringValues); result.ComboAfterJudgement = Combo.Value;
currentScoringValues.MaxCombo = HighestCombo.Value;
if (result.Type.AffectsAccuracy())
{
currentMaximumBaseScore += Judgement.ToNumericResult(result.Judgement.MaxResult);
currentBaseScore += Judgement.ToNumericResult(result.Type);
currentAccuracyJudgementCount++;
}
if (result.Type.IsBonus())
currentBonusPortion += GetBonusScoreChange(result);
else
currentComboPortion += GetComboScoreChange(result);
ApplyScoreChange(result);
hitEvents.Add(CreateHitEvent(result)); hitEvents.Add(CreateHitEvent(result));
lastHitObject = result.HitObject; lastHitObject = result.HitObject;
@ -221,20 +232,6 @@ namespace osu.Game.Rulesets.Scoring
updateScore(); updateScore();
} }
private static void applyResult(HitResult result, ref ScoringValues scoringValues)
{
if (!result.IsScorable())
return;
if (result.IsBonus())
scoringValues.BonusScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
else
scoringValues.BaseScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
if (result.IsBasic())
scoringValues.CountBasicHitObjects++;
}
/// <summary> /// <summary>
/// Creates the <see cref="HitEvent"/> that describes a <see cref="JudgementResult"/>. /// Creates the <see cref="HitEvent"/> that describes a <see cref="JudgementResult"/>.
/// </summary> /// </summary>
@ -253,15 +250,22 @@ namespace osu.Game.Rulesets.Scoring
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1; scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1;
// Always update the maximum scoring values.
revertResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
currentMaximumScoringValues.MaxCombo -= result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
if (!result.Type.IsScorable()) if (!result.Type.IsScorable())
return; return;
revertResult(result.Type, ref currentScoringValues); if (result.Type.AffectsAccuracy())
currentScoringValues.MaxCombo = HighestCombo.Value; {
currentMaximumBaseScore -= Judgement.ToNumericResult(result.Judgement.MaxResult);
currentBaseScore -= Judgement.ToNumericResult(result.Type);
currentAccuracyJudgementCount--;
}
if (result.Type.IsBonus())
currentBonusPortion -= GetBonusScoreChange(result);
else
currentComboPortion -= GetComboScoreChange(result);
RemoveScoreChange(result);
Debug.Assert(hitEvents.Count > 0); Debug.Assert(hitEvents.Count > 0);
lastHitObject = hitEvents[^1].LastHitObject; lastHitObject = hitEvents[^1].LastHitObject;
@ -270,110 +274,35 @@ namespace osu.Game.Rulesets.Scoring
updateScore(); updateScore();
} }
private static void revertResult(HitResult result, ref ScoringValues scoringValues) protected virtual double GetBonusScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type);
protected virtual double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type) * (1 + result.ComboAfterJudgement / 10d);
protected virtual void ApplyScoreChange(JudgementResult result)
{ {
if (!result.IsScorable()) }
return;
if (result.IsBonus()) protected virtual void RemoveScoreChange(JudgementResult result)
scoringValues.BonusScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0; {
else
scoringValues.BaseScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
if (result.IsBasic())
scoringValues.CountBasicHitObjects--;
} }
private void updateScore() private void updateScore()
{ {
Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1; Accuracy.Value = currentMaximumBaseScore > 0 ? currentBaseScore / currentMaximumBaseScore : 1;
MinimumAccuracy.Value = maximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / maximumScoringValues.BaseScore : 0; MinimumAccuracy.Value = maximumBaseScore > 0 ? currentBaseScore / maximumBaseScore : 0;
MaximumAccuracy.Value = maximumScoringValues.BaseScore > 0 MaximumAccuracy.Value = maximumBaseScore > 0 ? (currentBaseScore + (maximumBaseScore - currentMaximumBaseScore)) / maximumBaseScore : 1;
? (double)(currentScoringValues.BaseScore + (maximumScoringValues.BaseScore - currentMaximumScoringValues.BaseScore)) / maximumScoringValues.BaseScore
: 1; double comboProgress = maximumComboPortion > 0 ? currentComboPortion / maximumComboPortion : 1;
TotalScore.Value = computeScore(Mode.Value, currentScoringValues, maximumScoringValues); double accuracyProcess = maximumAccuracyJudgementCount > 0 ? (double)currentAccuracyJudgementCount / maximumAccuracyJudgementCount : 1;
TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion) * scoreMultiplier);
} }
/// <summary> protected virtual double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
/// Computes the accuracy of a given <see cref="ScoreInfo"/>.
/// </summary>
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
/// <returns>The score's accuracy.</returns>
[Pure]
public double ComputeAccuracy(ScoreInfo scoreInfo)
{ {
if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) return 700000 * comboProgress +
throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); 300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress +
bonusPortion;
// We only extract scoring values from the score's statistics. This is because accuracy is always relative to the point of pass or fail rather than relative to the whole beatmap.
extractScoringValues(scoreInfo.Statistics, out var current, out var maximum);
return maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1;
}
/// <summary>
/// Computes the total score of a given <see cref="ScoreInfo"/>.
/// </summary>
/// <remarks>
/// Does not require <see cref="JudgementProcessor.ApplyBeatmap"/> to have been called before use.
/// </remarks>
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
[Pure]
public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo)
{
if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
extractScoringValues(scoreInfo, out var current, out var maximum);
return computeScore(mode, current, maximum);
}
/// <summary>
/// Computes the total score from scoring values.
/// </summary>
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="current">The current scoring values.</param>
/// <param name="maximum">The maximum scoring values.</param>
/// <returns>The total score computed from the given scoring values.</returns>
[Pure]
private long computeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum)
{
double accuracyRatio = maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1;
double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1;
return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
}
/// <summary>
/// Computes the total score from individual scoring components.
/// </summary>
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="accuracyRatio">The accuracy percentage achieved by the player.</param>
/// <param name="comboRatio">The portion of the max combo achieved by the player.</param>
/// <param name="bonusScore">The total bonus score.</param>
/// <param name="totalBasicHitObjects">The total number of basic (non-tick and non-bonus) hitobjects in the beatmap.</param>
/// <returns>The total score computed from the given scoring component ratios.</returns>
[Pure]
public long ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, long bonusScore, int totalBasicHitObjects)
{
double accuracyScore = accuracyPortion * accuracyRatio;
double comboScore = comboPortion * comboRatio;
double rawScore = (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier;
switch (mode)
{
default:
case ScoringMode.Standardised:
return (long)Math.Round(rawScore);
case ScoringMode.Classic:
// This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring.
// The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes.
double scaledRawScore = rawScore / max_score;
return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier);
}
} }
/// <summary> /// <summary>
@ -389,16 +318,24 @@ namespace osu.Game.Rulesets.Scoring
if (storeResults) if (storeResults)
{ {
maximumScoringValues = currentScoringValues; maximumBaseScore = currentBaseScore;
maximumComboPortion = currentComboPortion;
maximumAccuracyJudgementCount = currentAccuracyJudgementCount;
maximumResultCounts.Clear(); maximumResultCounts.Clear();
maximumResultCounts.AddRange(scoreResultCounts); maximumResultCounts.AddRange(scoreResultCounts);
MaximumTotalScore = TotalScore.Value;
} }
scoreResultCounts.Clear(); scoreResultCounts.Clear();
currentScoringValues = default; currentBaseScore = 0;
currentMaximumScoringValues = default; currentMaximumBaseScore = 0;
currentAccuracyJudgementCount = 0;
currentComboPortion = 0;
currentBonusPortion = 0;
TotalScore.Value = 0; TotalScore.Value = 0;
Accuracy.Value = 1; Accuracy.Value = 1;
@ -428,7 +365,7 @@ namespace osu.Game.Rulesets.Scoring
score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result); score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result);
// Populate total score after everything else. // Populate total score after everything else.
score.TotalScore = ComputeScore(ScoringMode.Standardised, score); score.TotalScore = TotalScore.Value;
} }
/// <summary> /// <summary>
@ -452,126 +389,36 @@ namespace osu.Game.Rulesets.Scoring
if (frame.Header == null) if (frame.Header == null)
return; return;
extractScoringValues(frame.Header.Statistics, out var current, out var maximum);
currentScoringValues.BaseScore = current.BaseScore;
currentScoringValues.MaxCombo = frame.Header.MaxCombo;
currentMaximumScoringValues.BaseScore = maximum.BaseScore;
currentMaximumScoringValues.MaxCombo = maximum.MaxCombo;
Combo.Value = frame.Header.Combo; Combo.Value = frame.Header.Combo;
HighestCombo.Value = frame.Header.MaxCombo; HighestCombo.Value = frame.Header.MaxCombo;
TotalScore.Value = frame.Header.TotalScore;
scoreResultCounts.Clear(); scoreResultCounts.Clear();
scoreResultCounts.AddRange(frame.Header.Statistics); scoreResultCounts.AddRange(frame.Header.Statistics);
SetScoreProcessorStatistics(frame.Header.ScoreProcessorStatistics);
updateScore(); updateScore();
OnResetFromReplayFrame?.Invoke(); OnResetFromReplayFrame?.Invoke();
} }
#region ScoringValue extraction public ScoreProcessorStatistics GetScoreProcessorStatistics() => new ScoreProcessorStatistics
/// <summary>
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
/// </summary>
/// <remarks>
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
/// <list type="bullet">
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
/// </list>
/// Consumers are expected to more accurately fill in the above values through external means.
/// <para>
/// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.CountBasicHitObjects"/> for use in
/// <see cref="computeScore(osu.Game.Rulesets.Scoring.ScoringMode,ScoringValues,ScoringValues)"/>.
/// </para>
/// </remarks>
/// <param name="scoreInfo">The score to extract scoring values from.</param>
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
[Pure]
private void extractScoringValues(ScoreInfo scoreInfo, out ScoringValues current, out ScoringValues maximum)
{ {
extractScoringValues(scoreInfo.Statistics, out current, out maximum); MaximumBaseScore = currentMaximumBaseScore,
current.MaxCombo = scoreInfo.MaxCombo; BaseScore = currentBaseScore,
AccuracyJudgementCount = currentAccuracyJudgementCount,
ComboPortion = currentComboPortion,
BonusPortion = currentBonusPortion
};
if (scoreInfo.MaximumStatistics.Count > 0) public void SetScoreProcessorStatistics(ScoreProcessorStatistics statistics)
extractScoringValues(scoreInfo.MaximumStatistics, out _, out maximum);
}
/// <summary>
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
/// </summary>
/// <remarks>
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
/// <list type="bullet">
/// <item>The current <see cref="ScoringValues.MaxCombo"/> will always be 0.</item>
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
/// </list>
/// Consumers are expected to more accurately fill in the above values (especially the current <see cref="ScoringValues.MaxCombo"/>) via external means (e.g. <see cref="ScoreInfo"/>).
/// </remarks>
/// <param name="statistics">The hit statistics to extract scoring values from.</param>
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
[Pure]
private void extractScoringValues(IReadOnlyDictionary<HitResult, int> statistics, out ScoringValues current, out ScoringValues maximum)
{ {
current = default; currentMaximumBaseScore = statistics.MaximumBaseScore;
maximum = default; currentBaseScore = statistics.BaseScore;
currentAccuracyJudgementCount = statistics.AccuracyJudgementCount;
foreach ((HitResult result, int count) in statistics) currentComboPortion = statistics.ComboPortion;
{ currentBonusPortion = statistics.BonusPortion;
if (!result.IsScorable())
continue;
if (result.IsBonus())
current.BonusScore += count * Judgement.ToNumericResult(result);
if (result.AffectsAccuracy())
{
// The maximum result of this judgement if it wasn't a miss.
// E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT).
HitResult maxResult;
switch (result)
{
case HitResult.LargeTickHit:
case HitResult.LargeTickMiss:
maxResult = HitResult.LargeTickHit;
break;
case HitResult.SmallTickHit:
case HitResult.SmallTickMiss:
maxResult = HitResult.SmallTickHit;
break;
default:
maxResult = maxBasicResult ??= Ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result;
break;
}
current.BaseScore += count * Judgement.ToNumericResult(result);
maximum.BaseScore += count * Judgement.ToNumericResult(maxResult);
}
if (result.AffectsCombo())
maximum.MaxCombo += count;
if (result.IsBasic())
{
current.CountBasicHitObjects += count;
maximum.CountBasicHitObjects += count;
}
}
}
#endregion
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
hitEvents.Clear();
} }
#region Static helper methods #region Static helper methods
@ -630,30 +477,10 @@ namespace osu.Game.Rulesets.Scoring
#endregion #endregion
/// <summary> protected override void Dispose(bool isDisposing)
/// Stores the required scoring data that fulfils the minimum requirements for a <see cref="ScoreProcessor"/> to calculate score.
/// </summary>
private struct ScoringValues
{ {
/// <summary> base.Dispose(isDisposing);
/// The sum of all "basic" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBasic"/> and <see cref="Judgement.ToNumericResult"/>. hitEvents.Clear();
/// </summary>
public long BaseScore;
/// <summary>
/// The sum of all "bonus" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBonus"/> and <see cref="Judgement.ToNumericResult"/>.
/// </summary>
public long BonusScore;
/// <summary>
/// The highest achieved combo.
/// </summary>
public int MaxCombo;
/// <summary>
/// The count of "basic" <see cref="HitObject"/>s. See: <see cref="HitResultExtensions.IsBasic"/>.
/// </summary>
public int CountBasicHitObjects;
} }
} }
@ -665,4 +492,46 @@ namespace osu.Game.Rulesets.Scoring
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.ClassicScoreDisplay))] [LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.ClassicScoreDisplay))]
Classic Classic
} }
[Serializable]
[MessagePackObject]
public class ScoreProcessorStatistics
{
/// <summary>
/// The sum of all accuracy-affecting judgements at the current point in time.
/// </summary>
/// <remarks>
/// Used to compute accuracy.
/// See: <see cref="HitResultExtensions.IsBasic"/> and <see cref="Judgement.ToNumericResult"/>.
/// </remarks>
[Key(0)]
public double BaseScore { get; set; }
/// <summary>
/// The maximum sum of accuracy-affecting judgements at the current point in time.
/// </summary>
/// <remarks>
/// Used to compute accuracy.
/// </remarks>
[Key(1)]
public double MaximumBaseScore { get; set; }
/// <summary>
/// The count of accuracy-affecting judgements at the current point in time.
/// </summary>
[Key(2)]
public int AccuracyJudgementCount { get; set; }
/// <summary>
/// The combo score at the current point in time.
/// </summary>
[Key(3)]
public double ComboPortion { get; set; }
/// <summary>
/// The bonus score at the current point in time.
/// </summary>
[Key(4)]
public double BonusPortion { get; set; }
}
} }

View File

@ -15,6 +15,9 @@ namespace osu.Game.Scoring
{ {
IUser User { get; } IUser User { get; }
/// <summary>
/// The standardised total score.
/// </summary>
long TotalScore { get; } long TotalScore { get; }
int MaxCombo { get; } int MaxCombo { get; }

View File

@ -3,13 +3,72 @@
#nullable disable #nullable disable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring.Legacy namespace osu.Game.Scoring.Legacy
{ {
public static class ScoreInfoExtensions public static class ScoreInfoExtensions
{ {
public static long GetDisplayScore(this ScoreProcessor scoreProcessor, ScoringMode mode)
=> getDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics);
public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode)
=> getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics);
private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary<HitResult, int> maximumStatistics)
{
if (mode == ScoringMode.Standardised)
return score;
int maxBasicJudgements = maximumStatistics
.Where(k => k.Key.IsBasic())
.Select(k => k.Value)
.DefaultIfEmpty(0)
.Sum();
// This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring.
// The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes.
double scaledRawScore = score / ScoreProcessor.MAX_SCORE;
return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, maxBasicJudgements), 2) * getStandardisedToClassicMultiplier(rulesetId));
}
/// <summary>
/// Returns a ballpark multiplier which gives a similar "feel" for how large scores should get when displayed in "classic" mode.
/// This is different per ruleset to match the different algorithms used in the scoring implementation.
/// </summary>
private static double getStandardisedToClassicMultiplier(int rulesetId)
{
double multiplier;
switch (rulesetId)
{
// For non-legacy rulesets, just go with the same as the osu! ruleset.
// This is arbitrary, but at least allows the setting to do something to the score.
default:
case 0:
multiplier = 36;
break;
case 1:
multiplier = 22;
break;
case 2:
multiplier = 28;
break;
case 3:
multiplier = 16;
break;
}
return multiplier;
}
public static int? GetCountGeki(this ScoreInfo scoreInfo) public static int? GetCountGeki(this ScoreInfo scoreInfo)
{ {
switch (scoreInfo.Ruleset.OnlineID) switch (scoreInfo.Ruleset.OnlineID)

View File

@ -20,6 +20,7 @@ using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Scoring.Legacy;
namespace osu.Game.Scoring namespace osu.Game.Scoring
{ {
@ -74,7 +75,7 @@ namespace osu.Game.Scoring
/// <param name="scores">The array of <see cref="ScoreInfo"/>s to reorder.</param> /// <param name="scores">The array of <see cref="ScoreInfo"/>s to reorder.</param>
/// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns> /// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns>
public IEnumerable<ScoreInfo> OrderByTotalScore(IEnumerable<ScoreInfo> scores) public IEnumerable<ScoreInfo> OrderByTotalScore(IEnumerable<ScoreInfo> scores)
=> scores.OrderByDescending(s => GetTotalScore(s)) => scores.OrderByDescending(s => s.TotalScore)
.ThenBy(s => s.OnlineID) .ThenBy(s => s.OnlineID)
// Local scores may not have an online ID. Fall back to date in these cases. // Local scores may not have an online ID. Fall back to date in these cases.
.ThenBy(s => s.Date); .ThenBy(s => s.Date);
@ -87,7 +88,7 @@ namespace osu.Game.Scoring
/// </remarks> /// </remarks>
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param> /// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
/// <returns>The bindable containing the total score.</returns> /// <returns>The bindable containing the total score.</returns>
public Bindable<long> GetBindableTotalScore([NotNull] ScoreInfo score) => new TotalScoreBindable(score, this, configManager); public Bindable<long> GetBindableTotalScore([NotNull] ScoreInfo score) => new TotalScoreBindable(score, configManager);
/// <summary> /// <summary>
/// Retrieves a bindable that represents the formatted total score string of a <see cref="ScoreInfo"/>. /// Retrieves a bindable that represents the formatted total score string of a <see cref="ScoreInfo"/>.
@ -99,25 +100,6 @@ namespace osu.Game.Scoring
/// <returns>The bindable containing the formatted total score string.</returns> /// <returns>The bindable containing the formatted total score string.</returns>
public Bindable<string> GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score)); public Bindable<string> GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score));
/// <summary>
/// Retrieves the total score of a <see cref="ScoreInfo"/> in the given <see cref="ScoringMode"/>.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to calculate the total score of.</param>
/// <param name="mode">The <see cref="ScoringMode"/> to return the total score as.</param>
/// <returns>The total score.</returns>
public long GetTotalScore([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised)
{
// TODO: This is required for playlist aggregate scores. They should likely not be getting here in the first place.
if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash))
return score.TotalScore;
var ruleset = score.Ruleset.CreateInstance();
var scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = score.Mods;
return scoreProcessor.ComputeScore(mode, score);
}
/// <summary> /// <summary>
/// Retrieves the maximum achievable combo for the provided score. /// Retrieves the maximum achievable combo for the provided score.
/// </summary> /// </summary>
@ -136,12 +118,11 @@ namespace osu.Game.Scoring
/// Creates a new <see cref="TotalScoreBindable"/>. /// Creates a new <see cref="TotalScoreBindable"/>.
/// </summary> /// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to provide the total score of.</param> /// <param name="score">The <see cref="ScoreInfo"/> to provide the total score of.</param>
/// <param name="scoreManager">The <see cref="ScoreManager"/>.</param>
/// <param name="configManager">The config.</param> /// <param name="configManager">The config.</param>
public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager, OsuConfigManager configManager) public TotalScoreBindable(ScoreInfo score, OsuConfigManager configManager)
{ {
configManager?.BindWith(OsuSetting.ScoreDisplayMode, scoringMode); configManager?.BindWith(OsuSetting.ScoreDisplayMode, scoringMode);
scoringMode.BindValueChanged(mode => Value = scoreManager.GetTotalScore(score, mode.NewValue), true); scoringMode.BindValueChanged(mode => Value = score.GetDisplayScore(mode.NewValue), true);
} }
} }

View File

@ -6,14 +6,12 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -63,13 +61,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return new PlaylistsResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, true); return new PlaylistsResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, true);
} }
protected override async Task PrepareScoreForResultsAsync(Score score)
{
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
Score.ScoreInfo.TotalScore = ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo);
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@ -1,18 +1,17 @@
// 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 JetBrains.Annotations;
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;
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.Graphics.Shapes;
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.Rulesets.Scoring;
using osu.Game.Users; using osu.Game.Users;
using osu.Game.Users.Drawables; using osu.Game.Users.Drawables;
using osu.Game.Utils; using osu.Game.Utils;
@ -48,7 +47,7 @@ namespace osu.Game.Screens.Play.HUD
public Bindable<bool> Expanded = new Bindable<bool>(); public Bindable<bool> Expanded = new Bindable<bool>();
private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText; private OsuSpriteText positionText = null!, scoreText = null!, accuracyText = null!, comboText = null!, usernameText = null!;
public BindableLong TotalScore { get; } = new BindableLong(); public BindableLong TotalScore { get; } = new BindableLong();
public BindableDouble Accuracy { get; } = new BindableDouble(1); public BindableDouble Accuracy { get; } = new BindableDouble(1);
@ -56,6 +55,13 @@ namespace osu.Game.Screens.Play.HUD
public BindableBool HasQuit { get; } = new BindableBool(); public BindableBool HasQuit { get; } = new BindableBool();
public Bindable<long> DisplayOrder { get; } = new Bindable<long>(); public Bindable<long> DisplayOrder { get; } = new Bindable<long>();
private Func<ScoringMode, long>? getDisplayScoreFunction;
public Func<ScoringMode, long> GetDisplayScore
{
set => getDisplayScoreFunction = value;
}
public Color4? BackgroundColour { get; set; } public Color4? BackgroundColour { get; set; }
public Color4? TextColour { get; set; } public Color4? TextColour { get; set; }
@ -82,40 +88,43 @@ namespace osu.Game.Screens.Play.HUD
} }
} }
[CanBeNull] public IUser? User { get; }
public IUser User { get; }
/// <summary> /// <summary>
/// Whether this score is the local user or a replay player (and should be focused / always visible). /// Whether this score is the local user or a replay player (and should be focused / always visible).
/// </summary> /// </summary>
public readonly bool Tracked; public readonly bool Tracked;
private Container mainFillContainer; private Container mainFillContainer = null!;
private Box centralFill; private Box centralFill = null!;
private Container backgroundPaddingAdjustContainer; private Container backgroundPaddingAdjustContainer = null!;
private GridContainer gridContainer; private GridContainer gridContainer = null!;
private Container scoreComponents; private Container scoreComponents = null!;
private IBindable<ScoringMode> scoreDisplayMode = null!;
/// <summary> /// <summary>
/// Creates a new <see cref="GameplayLeaderboardScore"/>. /// Creates a new <see cref="GameplayLeaderboardScore"/>.
/// </summary> /// </summary>
/// <param name="user">The score's player.</param> /// <param name="user">The score's player.</param>
/// <param name="tracked">Whether the player is the local user or a replay player.</param> /// <param name="tracked">Whether the player is the local user or a replay player.</param>
public GameplayLeaderboardScore([CanBeNull] IUser user, bool tracked) public GameplayLeaderboardScore(IUser? user, bool tracked)
{ {
User = user; User = user;
Tracked = tracked; Tracked = tracked;
AutoSizeAxes = Axes.X; AutoSizeAxes = Axes.X;
Height = PANEL_HEIGHT; Height = PANEL_HEIGHT;
GetDisplayScore = _ => TotalScore.Value;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours, OsuConfigManager osuConfigManager)
{ {
Container avatarContainer; Container avatarContainer;
@ -234,7 +243,7 @@ namespace osu.Game.Screens.Play.HUD
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Colour = Color4.White, Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
Text = User?.Username, Text = User?.Username ?? string.Empty,
Truncate = true, Truncate = true,
Shadow = false, Shadow = false,
} }
@ -286,7 +295,9 @@ namespace osu.Game.Screens.Play.HUD
LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add); LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add);
TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true); scoreDisplayMode = osuConfigManager.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
scoreDisplayMode.BindValueChanged(_ => updateScore());
TotalScore.BindValueChanged(_ => updateScore(), true);
Accuracy.BindValueChanged(v => Accuracy.BindValueChanged(v =>
{ {
@ -313,6 +324,8 @@ namespace osu.Game.Screens.Play.HUD
FinishTransforms(true); FinishTransforms(true);
} }
private void updateScore() => scoreText.Text = (getDisplayScoreFunction?.Invoke(scoreDisplayMode.Value) ?? TotalScore.Value).ToString("N0");
private void changeExpandedState(ValueChangedEvent<bool> expanded) private void changeExpandedState(ValueChangedEvent<bool> expanded)
{ {
if (expanded.NewValue) if (expanded.NewValue)

View File

@ -1,20 +1,21 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring.Legacy;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public abstract partial class GameplayScoreCounter : ScoreCounter public abstract partial class GameplayScoreCounter : ScoreCounter
{ {
private Bindable<ScoringMode> scoreDisplayMode; private Bindable<ScoringMode> scoreDisplayMode = null!;
private Bindable<long> totalScoreBindable = null!;
protected GameplayScoreCounter() protected GameplayScoreCounter()
: base(6) : base(6)
@ -24,6 +25,9 @@ namespace osu.Game.Screens.Play.HUD
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config, ScoreProcessor scoreProcessor) private void load(OsuConfigManager config, ScoreProcessor scoreProcessor)
{ {
totalScoreBindable = scoreProcessor.TotalScore.GetBoundCopy();
totalScoreBindable.BindValueChanged(_ => updateDisplayScore());
scoreDisplayMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode); scoreDisplayMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
scoreDisplayMode.BindValueChanged(scoreMode => scoreDisplayMode.BindValueChanged(scoreMode =>
{ {
@ -40,9 +44,11 @@ namespace osu.Game.Screens.Play.HUD
default: default:
throw new ArgumentOutOfRangeException(nameof(scoreMode)); throw new ArgumentOutOfRangeException(nameof(scoreMode));
} }
updateDisplayScore();
}, true); }, true);
Current.BindTo(scoreProcessor.TotalScore); void updateDisplayScore() => Current.Value = scoreProcessor.GetDisplayScore(scoreDisplayMode.Value);
} }
} }
} }

View File

@ -1,9 +1,9 @@
// 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 osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
@ -20,5 +20,12 @@ namespace osu.Game.Screens.Play.HUD
/// Lower numbers will appear higher in cases of <see cref="TotalScore"/> ties. /// Lower numbers will appear higher in cases of <see cref="TotalScore"/> ties.
/// </summary> /// </summary>
Bindable<long> DisplayOrder { get; } Bindable<long> DisplayOrder { get; }
/// <summary>
/// A custom function which handles converting a score to a display score using a provide <see cref="ScoringMode"/>.
/// </summary>
/// <remarks>
/// If no function is provided, <see cref="TotalScore"/> will be used verbatim.</remarks>
Func<ScoringMode, long> GetDisplayScore { set; }
} }
} }

View File

@ -98,6 +98,7 @@ namespace osu.Game.Screens.Play.HUD
var trackedUser = UserScores[user.Id]; var trackedUser = UserScores[user.Id];
var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id); var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id);
leaderboardScore.GetDisplayScore = trackedUser.ScoreProcessor.GetDisplayScore;
leaderboardScore.Accuracy.BindTo(trackedUser.ScoreProcessor.Accuracy); leaderboardScore.Accuracy.BindTo(trackedUser.ScoreProcessor.Accuracy);
leaderboardScore.TotalScore.BindTo(trackedUser.ScoreProcessor.TotalScore); leaderboardScore.TotalScore.BindTo(trackedUser.ScoreProcessor.TotalScore);
leaderboardScore.Combo.BindTo(trackedUser.ScoreProcessor.Combo); leaderboardScore.Combo.BindTo(trackedUser.ScoreProcessor.Combo);

View File

@ -1,7 +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.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -10,6 +9,7 @@ using osu.Game.Configuration;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Users; using osu.Game.Users;
@ -27,15 +27,9 @@ namespace osu.Game.Screens.Play.HUD
public readonly IBindableList<ScoreInfo> Scores = new BindableList<ScoreInfo>(); public readonly IBindableList<ScoreInfo> Scores = new BindableList<ScoreInfo>();
// hold references to ensure bindables are updated.
private readonly List<Bindable<long>> scoreBindables = new List<Bindable<long>>();
[Resolved] [Resolved]
private ScoreProcessor scoreProcessor { get; set; } = null!; private ScoreProcessor scoreProcessor { get; set; } = null!;
[Resolved]
private ScoreManager scoreManager { get; set; } = null!;
/// <summary> /// <summary>
/// Whether the leaderboard should be visible regardless of the configuration value. /// Whether the leaderboard should be visible regardless of the configuration value.
/// This is true by default, but can be changed. /// This is true by default, but can be changed.
@ -70,7 +64,6 @@ namespace osu.Game.Screens.Play.HUD
private void showScores() private void showScores()
{ {
Clear(); Clear();
scoreBindables.Clear();
if (!Scores.Any()) if (!Scores.Any())
return; return;
@ -79,12 +72,8 @@ namespace osu.Game.Screens.Play.HUD
{ {
var score = Add(s.User, false); var score = Add(s.User, false);
var bindableTotal = scoreManager.GetBindableTotalScore(s); score.GetDisplayScore = s.GetDisplayScore;
score.TotalScore.Value = s.TotalScore;
// Direct binding not possible due to differing types (see https://github.com/ppy/osu/issues/20298).
bindableTotal.BindValueChanged(total => score.TotalScore.Value = total.NewValue, true);
scoreBindables.Add(bindableTotal);
score.Accuracy.Value = s.Accuracy; score.Accuracy.Value = s.Accuracy;
score.Combo.Value = s.MaxCombo; score.Combo.Value = s.MaxCombo;
score.DisplayOrder.Value = s.OnlineID > 0 ? s.OnlineID : s.Date.ToUnixTimeSeconds(); score.DisplayOrder.Value = s.OnlineID > 0 ? s.OnlineID : s.Date.ToUnixTimeSeconds();
@ -92,6 +81,7 @@ namespace osu.Game.Screens.Play.HUD
ILeaderboardScore local = Add(trackingUser, true); ILeaderboardScore local = Add(trackingUser, true);
local.GetDisplayScore = scoreProcessor.GetDisplayScore;
local.TotalScore.BindTarget = scoreProcessor.TotalScore; local.TotalScore.BindTarget = scoreProcessor.TotalScore;
local.Accuracy.BindTarget = scoreProcessor.Accuracy; local.Accuracy.BindTarget = scoreProcessor.Accuracy;
local.Combo.BindTarget = scoreProcessor.HighestCombo; local.Combo.BindTarget = scoreProcessor.HighestCombo;

View File

@ -237,9 +237,6 @@ namespace osu.Game.Screens.Play
dependencies.CacheAs(HealthProcessor); dependencies.CacheAs(HealthProcessor);
if (!ScoreProcessor.Mode.Disabled)
config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime);
AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));

View File

@ -9,7 +9,6 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using JetBrains.Annotations; using JetBrains.Annotations;
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;
@ -67,9 +66,6 @@ namespace osu.Game.Screens.Ranking
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>(); public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
[Resolved]
private ScoreManager scoreManager { get; set; }
private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource();
private readonly Flow flow; private readonly Flow flow;
private readonly Scroll scroll; private readonly Scroll scroll;
@ -149,7 +145,7 @@ namespace osu.Game.Screens.Ranking
var score = trackingContainer.Panel.Score; var score = trackingContainer.Panel.Score;
flow.SetLayoutPosition(trackingContainer, scoreManager.GetTotalScore(score)); flow.SetLayoutPosition(trackingContainer, score.TotalScore);
trackingContainer.Show(); trackingContainer.Show();

View File

@ -12,7 +12,9 @@ using osu.Framework.Utils;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Tests.Visual.Spectator namespace osu.Game.Tests.Visual.Spectator
@ -44,6 +46,9 @@ namespace osu.Game.Tests.Visual.Spectator
[Resolved] [Resolved]
private IAPIProvider api { get; set; } = null!; private IAPIProvider api { get; set; } = null!;
[Resolved]
private RulesetStore rulesetStore { get; set; } = null!;
public TestSpectatorClient() public TestSpectatorClient()
{ {
OnNewFrames += (i, bundle) => lastReceivedUserFrames[i] = bundle.Frames[^1]; OnNewFrames += (i, bundle) => lastReceivedUserFrames[i] = bundle.Frames[^1];
@ -119,7 +124,7 @@ namespace osu.Game.Tests.Visual.Spectator
if (frames.Count == 0) if (frames.Count == 0)
return; return;
var bundle = new FrameDataBundle(new ScoreInfo { Combo = currentFrameIndex }, frames.ToArray()); var bundle = new FrameDataBundle(new ScoreInfo { Combo = currentFrameIndex }, new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance()), frames.ToArray());
((ISpectatorClient)this).UserSentFrames(userId, bundle); ((ISpectatorClient)this).UserSentFrames(userId, bundle);
frames.Clear(); frames.Clear();