mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 07:42:57 +08:00
Merge pull request #23638 from smoogipoo/scorev2
Replace lazer scoring with "ScoreV2"
This commit is contained in:
commit
a436f858df
@ -1,17 +1,30 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Scoring
|
||||
{
|
||||
public partial class CatchScoreProcessor : ScoreProcessor
|
||||
{
|
||||
private const int combo_cap = 200;
|
||||
private const double combo_base = 4;
|
||||
|
||||
public CatchScoreProcessor()
|
||||
: 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));
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,29 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.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()
|
||||
: 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 ClassicScoreMultiplier => 16;
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using System;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Scoring
|
||||
@ -18,21 +13,11 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
}
|
||||
|
||||
protected override double ClassicScoreMultiplier => 36;
|
||||
|
||||
protected override HitEvent CreateHitEvent(JudgementResult result)
|
||||
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
|
||||
|
||||
protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement)
|
||||
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case HitCircle:
|
||||
return new OsuHitCircleJudgementResult(hitObject, judgement);
|
||||
|
||||
default:
|
||||
return new OsuJudgementResult(hitObject, judgement);
|
||||
}
|
||||
return 700000 * comboProgress
|
||||
+ 300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress
|
||||
+ bonusPortion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,8 +91,9 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
{
|
||||
prepareDrawableRulesetAndBeatmap(false);
|
||||
|
||||
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
|
||||
assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle);
|
||||
var hit = new Hit();
|
||||
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]
|
||||
|
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
|
||||
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,
|
||||
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.
|
||||
public override Judgement CreateJudgement() => new IgnoreJudgement();
|
||||
|
||||
public StrongNestedHit(TaikoHitObject parent)
|
||||
: base(parent)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
#region LegacyBeatmapEncoder
|
||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
|
||||
public override double MaximumJudgementOffset => HitWindow;
|
||||
|
||||
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit
|
||||
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit(this)
|
||||
{
|
||||
StartTime = startTime,
|
||||
Samples = Samples
|
||||
@ -41,6 +41,10 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
|
||||
public class StrongNestedHit : StrongNestedHitObject
|
||||
{
|
||||
public StrongNestedHit(TaikoHitObject parent)
|
||||
: base(parent)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
Samples = Samples
|
||||
@ -80,6 +80,10 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
|
||||
public class StrongNestedHit : StrongNestedHitObject
|
||||
{
|
||||
public StrongNestedHit(TaikoHitObject parent)
|
||||
: base(parent)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,13 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
/// </summary>
|
||||
public abstract class StrongNestedHitObject : TaikoHitObject
|
||||
{
|
||||
public readonly TaikoHitObject Parent;
|
||||
|
||||
protected StrongNestedHitObject(TaikoHitObject parent)
|
||||
{
|
||||
Parent = parent;
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => new TaikoStrongJudgement();
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
@ -1,23 +1,44 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
{
|
||||
internal partial class TaikoScoreProcessor : ScoreProcessor
|
||||
public partial class TaikoScoreProcessor : ScoreProcessor
|
||||
{
|
||||
private const double combo_base = 4;
|
||||
|
||||
public TaikoScoreProcessor()
|
||||
: 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -76,22 +76,38 @@ namespace osu.Game.Tests.Gameplay
|
||||
// Reset with a miss instead.
|
||||
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.JudgedHits, Is.EqualTo(1));
|
||||
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
|
||||
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(0));
|
||||
|
||||
// Reset with no judged hit.
|
||||
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.JudgedHits, Is.Zero);
|
||||
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
|
||||
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -179,7 +179,7 @@ namespace osu.Game.Tests.Resources
|
||||
BeatmapHash = beatmap.Hash,
|
||||
Ruleset = beatmap.Ruleset,
|
||||
Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() },
|
||||
TotalScore = 2845370,
|
||||
TotalScore = 284537,
|
||||
Accuracy = 0.95,
|
||||
MaxCombo = 999,
|
||||
Position = 1,
|
||||
|
@ -14,11 +14,12 @@ using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Rulesets.Scoring
|
||||
@ -31,7 +32,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
scoreProcessor = new ScoreProcessor(new TestRuleset());
|
||||
scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
||||
beatmap = new TestBeatmap(new RulesetInfo())
|
||||
{
|
||||
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.Ok, 800_000)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, 116_667)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Ok, 233_338)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Meh, 20)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Ok, 23)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Meh, 0)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Ok, 2)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Great, 36)]
|
||||
public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
|
||||
{
|
||||
scoreProcessor.Mode.Value = scoringMode;
|
||||
scoreProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new OsuJudgement())
|
||||
@ -58,7 +58,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
};
|
||||
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>
|
||||
@ -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>
|
||||
/// <remarks>
|
||||
/// 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>
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] // (3 * 0) / (4 * 300) * 300_000 + (0 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] // (3 * 50) / (4 * 300) * 300_000 + (2 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] // (3 * 100) / (4 * 300) * 300_000 + (2 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 492_857)] // (3 * 200) / (4 * 350) * 300_000 + (2 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] // (3 * 300) / (4 * 300) * 300_000 + (2 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] // (3 * 350) / (4 * 350) * 300_000 + (2 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] // (3 * 0) / (4 * 10) * 300_000 + 700_000 (max combo 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.LargeTickMiss, HitResult.LargeTickHit, 0)] // (3 * 0) / (4 * 30) * 300_000 + (0 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] // (3 * 30) / (4 * 30) * 300_000 + (0 / 4) * 700_000
|
||||
[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.LargeBonus, HitResult.LargeBonus, 1_000_150)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points)
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 79_333)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 158_667)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 302_402)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 492_894)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 492_894)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 541_894)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 492_894)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 86)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 104)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 140)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 190)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 190)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 18)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 31)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 4)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 53)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 140)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 140)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 11)]
|
||||
[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.LargeBonus, HitResult.LargeBonus, 36)]
|
||||
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))
|
||||
};
|
||||
scoreProcessor.Mode.Value = scoringMode;
|
||||
scoreProcessor.ApplyBeatmap(fourObjectBeatmap);
|
||||
|
||||
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
|
||||
};
|
||||
scoreProcessor.ApplyResult(judgementResult);
|
||||
}
|
||||
|
||||
Assert.That(scoreProcessor.TotalScore.Value, 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));
|
||||
Assert.That(scoreProcessor.GetDisplayScore(scoringMode), Is.EqualTo(expectedScore).Within(0.5d));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -173,10 +122,9 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[Values(ScoringMode.Standardised, ScoringMode.Classic)]
|
||||
ScoringMode scoringMode)
|
||||
{
|
||||
scoreProcessor.Mode.Value = scoringMode;
|
||||
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)]
|
||||
@ -294,28 +242,6 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
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
|
||||
[Test]
|
||||
public void TestLegacyComboIncrease()
|
||||
@ -330,29 +256,6 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
Assert.That(HitResult.LegacyComboIncrease.IsHit(), Is.True);
|
||||
Assert.That(HitResult.LegacyComboIncrease.IsScorable(), Is.True);
|
||||
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
|
||||
|
||||
@ -362,36 +265,30 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
const int count_judgements = 1000;
|
||||
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++)
|
||||
{
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], new TestJudgement(HitResult.Great))
|
||||
{
|
||||
{ HitResult.Great, count_judgements - count_misses },
|
||||
{ HitResult.Miss, count_misses }
|
||||
}
|
||||
});
|
||||
Type = i == 0 ? HitResult.Miss : HitResult.Great
|
||||
});
|
||||
}
|
||||
|
||||
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(1.0));
|
||||
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
|
||||
{
|
||||
public override HitResult MaxResult { get; }
|
||||
@ -419,14 +316,18 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
|
||||
private partial class TestScoreProcessor : ScoreProcessor
|
||||
{
|
||||
protected override double DefaultAccuracyPortion => 0.5;
|
||||
protected override double DefaultComboPortion => 0.5;
|
||||
|
||||
public TestScoreProcessor()
|
||||
: 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
|
||||
private class TestRuleset : Ruleset
|
||||
{
|
||||
|
@ -31,8 +31,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private HUDOverlay hudOverlay = null!;
|
||||
|
||||
[Cached]
|
||||
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
||||
[Cached(typeof(ScoreProcessor))]
|
||||
private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
|
||||
|
||||
[Cached(typeof(HealthProcessor))]
|
||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||
|
@ -22,6 +22,7 @@ using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
@ -124,8 +125,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
graphs.Clear();
|
||||
legend.Clear();
|
||||
|
||||
runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Standardised } });
|
||||
runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Classic } });
|
||||
runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()), ScoringMode.Standardised);
|
||||
runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()), ScoringMode.Classic);
|
||||
|
||||
runScoreV1();
|
||||
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;
|
||||
|
||||
@ -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.Ok }),
|
||||
() => 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;
|
||||
|
||||
|
@ -22,8 +22,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneSkinEditorMultipleSkins : SkinnableTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
||||
[Cached(typeof(ScoreProcessor))]
|
||||
private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
|
||||
|
||||
[Cached(typeof(HealthProcessor))]
|
||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||
|
@ -28,8 +28,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
private HUDOverlay hudOverlay;
|
||||
|
||||
[Cached]
|
||||
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
||||
[Cached(typeof(ScoreProcessor))]
|
||||
private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
|
||||
|
||||
[Cached(typeof(HealthProcessor))]
|
||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||
|
@ -10,13 +10,14 @@ using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneSkinnableScoreCounter : SkinnableHUDComponentTestScene
|
||||
{
|
||||
[Cached]
|
||||
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
||||
[Cached(typeof(ScoreProcessor))]
|
||||
private ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor;
|
||||
|
||||
protected override Drawable CreateDefaultImplementation() => new DefaultScoreCounter();
|
||||
protected override Drawable CreateLegacyImplementation() => new LegacyScoreCounter();
|
||||
|
@ -16,13 +16,14 @@ using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneSoloGameplayLeaderboard : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
||||
[Cached(typeof(ScoreProcessor))]
|
||||
private readonly ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor;
|
||||
|
||||
private readonly BindableList<ScoreInfo> scores = new BindableList<ScoreInfo>();
|
||||
|
||||
|
@ -21,7 +21,6 @@ using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Replays.Legacy;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
@ -188,15 +187,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
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.Meh] = 0,
|
||||
[HitResult.Great] = 0
|
||||
}
|
||||
});
|
||||
[HitResult.Miss] = 0,
|
||||
[HitResult.Meh] = 0,
|
||||
[HitResult.Great] = 0
|
||||
}, new ScoreProcessorStatistics(), DateTimeOffset.Now);
|
||||
}
|
||||
|
||||
switch (RNG.Next(0, 3))
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@ -12,7 +11,6 @@ using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Tests.Resources;
|
||||
@ -143,25 +141,20 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
testScoreInfo.User = API.LocalUser.Value;
|
||||
testScoreInfo.Rank = ScoreRank.B;
|
||||
testScoreInfo.TotalScore = scoreManager.GetTotalScore(testScoreInfo, ScoringMode.Classic);
|
||||
|
||||
scoreManager.Import(testScoreInfo);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
testScoreInfo2.User = API.LocalUser.Value;
|
||||
testScoreInfo2.Rank = ScoreRank.X;
|
||||
testScoreInfo2.Statistics = testScoreInfo2.MaximumStatistics;
|
||||
testScoreInfo2.TotalScore = scoreManager.GetTotalScore(testScoreInfo2);
|
||||
|
||||
// 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);
|
||||
testScoreInfo2.TotalScore = testScoreInfo.TotalScore + 1;
|
||||
|
||||
scoreManager.Import(testScoreInfo2);
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Replays.Legacy;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Online.Spectator
|
||||
@ -20,10 +21,10 @@ namespace osu.Game.Online.Spectator
|
||||
[Key(1)]
|
||||
public IList<LegacyReplayFrame> Frames { get; set; }
|
||||
|
||||
public FrameDataBundle(ScoreInfo score, IList<LegacyReplayFrame> frames)
|
||||
public FrameDataBundle(ScoreInfo score, ScoreProcessor scoreProcessor, IList<LegacyReplayFrame> frames)
|
||||
{
|
||||
Frames = frames;
|
||||
Header = new FrameHeader(score);
|
||||
Header = new FrameHeader(score, scoreProcessor.GetScoreProcessorStatistics());
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
|
@ -15,57 +15,74 @@ namespace osu.Game.Online.Spectator
|
||||
public class FrameHeader
|
||||
{
|
||||
/// <summary>
|
||||
/// The current accuracy of the score.
|
||||
/// The total score.
|
||||
/// </summary>
|
||||
[Key(0)]
|
||||
public long TotalScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current accuracy of the score.
|
||||
/// </summary>
|
||||
[Key(1)]
|
||||
public double Accuracy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current combo of the score.
|
||||
/// </summary>
|
||||
[Key(1)]
|
||||
[Key(2)]
|
||||
public int Combo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum combo achieved up to the current point in time.
|
||||
/// </summary>
|
||||
[Key(2)]
|
||||
[Key(3)]
|
||||
public int MaxCombo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cumulative hit statistics.
|
||||
/// </summary>
|
||||
[Key(3)]
|
||||
[Key(4)]
|
||||
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>
|
||||
/// The time at which this frame was received by the server.
|
||||
/// </summary>
|
||||
[Key(4)]
|
||||
[Key(6)]
|
||||
public DateTimeOffset ReceivedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Construct header summary information from a point-in-time reference to a score which is actively being played.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
MaxCombo = score.MaxCombo;
|
||||
Accuracy = score.Accuracy;
|
||||
|
||||
// copy for safety
|
||||
Statistics = new Dictionary<HitResult, int>(score.Statistics);
|
||||
|
||||
ScoreProcessorStatistics = statistics;
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
[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;
|
||||
MaxCombo = maxCombo;
|
||||
Accuracy = accuracy;
|
||||
Statistics = statistics;
|
||||
ScoreProcessorStatistics = scoreProcessorStatistics;
|
||||
ReceivedTime = receivedTime;
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Replays.Legacy;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
@ -82,6 +83,7 @@ namespace osu.Game.Online.Spectator
|
||||
private IBeatmap? currentBeatmap;
|
||||
private Score? currentScore;
|
||||
private long? currentScoreToken;
|
||||
private ScoreProcessor? currentScoreProcessor;
|
||||
|
||||
private readonly Queue<FrameDataBundle> pendingFrameBundles = new Queue<FrameDataBundle>();
|
||||
|
||||
@ -192,6 +194,7 @@ namespace osu.Game.Online.Spectator
|
||||
currentBeatmap = state.Beatmap;
|
||||
currentScore = score;
|
||||
currentScoreToken = scoreToken;
|
||||
currentScoreProcessor = state.ScoreProcessor;
|
||||
|
||||
BeginPlayingInternal(currentScoreToken, currentState);
|
||||
});
|
||||
@ -302,9 +305,10 @@ namespace osu.Game.Online.Spectator
|
||||
return;
|
||||
|
||||
Debug.Assert(currentScore != null);
|
||||
Debug.Assert(currentScoreProcessor != null);
|
||||
|
||||
var frames = pendingFrames.ToArray();
|
||||
var bundle = new FrameDataBundle(currentScore.ScoreInfo, frames);
|
||||
var bundle = new FrameDataBundle(currentScore.ScoreInfo, currentScoreProcessor, frames);
|
||||
|
||||
pendingFrames.Clear();
|
||||
lastPurgeTime = Time.Current;
|
||||
|
@ -14,6 +14,7 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
|
||||
namespace osu.Game.Online.Spectator
|
||||
{
|
||||
@ -46,7 +47,9 @@ namespace osu.Game.Online.Spectator
|
||||
/// <summary>
|
||||
/// The applied <see cref="Mod"/>s.
|
||||
/// </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;
|
||||
|
||||
@ -70,7 +73,6 @@ namespace osu.Game.Online.Spectator
|
||||
private readonly int userId;
|
||||
|
||||
private SpectatorState? spectatorState;
|
||||
private ScoreProcessor? scoreProcessor;
|
||||
private ScoreInfo? scoreInfo;
|
||||
|
||||
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)
|
||||
{
|
||||
scoreProcessor?.RemoveAndDisposeImmediately();
|
||||
scoreProcessor = null;
|
||||
scoreInfo = null;
|
||||
spectatorState = null;
|
||||
replayFrames.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (scoreProcessor != null)
|
||||
if (scoreInfo != null)
|
||||
return;
|
||||
|
||||
Debug.Assert(scoreInfo == null);
|
||||
|
||||
RulesetInfo? rulesetInfo = rulesetStore.GetRuleset(userState.RulesetID.Value);
|
||||
if (rulesetInfo == null)
|
||||
return;
|
||||
@ -114,9 +112,11 @@ namespace osu.Game.Online.Spectator
|
||||
Ruleset ruleset = rulesetInfo.CreateInstance();
|
||||
|
||||
spectatorState = userState;
|
||||
scoreInfo = new ScoreInfo { Ruleset = rulesetInfo };
|
||||
scoreProcessor = ruleset.CreateScoreProcessor();
|
||||
scoreProcessor.Mods.Value = userState.Mods.Select(m => m.ToMod(ruleset)).ToArray();
|
||||
scoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = rulesetInfo,
|
||||
Mods = userState.Mods.Select(m => m.ToMod(ruleset)).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private void onNewFrames(int incomingUserId, FrameDataBundle bundle)
|
||||
@ -126,7 +126,7 @@ namespace osu.Game.Online.Spectator
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
if (scoreProcessor == null)
|
||||
if (scoreInfo == null)
|
||||
return;
|
||||
|
||||
replayFrames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
|
||||
@ -140,7 +140,6 @@ namespace osu.Game.Online.Spectator
|
||||
return;
|
||||
|
||||
Debug.Assert(spectatorState != null);
|
||||
Debug.Assert(scoreProcessor != null);
|
||||
|
||||
int frameIndex = replayFrames.BinarySearch(new TimedFrame(ReferenceClock.CurrentTime));
|
||||
if (frameIndex < 0)
|
||||
@ -150,14 +149,15 @@ namespace osu.Game.Online.Spectator
|
||||
TimedFrame frame = replayFrames[frameIndex];
|
||||
Debug.Assert(frame.Header != null);
|
||||
|
||||
scoreInfo.Accuracy = frame.Header.Accuracy;
|
||||
scoreInfo.MaxCombo = frame.Header.MaxCombo;
|
||||
scoreInfo.Statistics = frame.Header.Statistics;
|
||||
scoreInfo.MaximumStatistics = spectatorState.MaximumStatistics;
|
||||
scoreInfo.TotalScore = frame.Header.TotalScore;
|
||||
|
||||
Accuracy.Value = frame.Header.Accuracy;
|
||||
Combo.Value = frame.Header.Combo;
|
||||
|
||||
TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo);
|
||||
TotalScore.Value = frame.Header.TotalScore;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -62,11 +62,13 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
.GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count()))
|
||||
.ToDictionary(pair => pair.hitResult, pair => pair.count);
|
||||
perfectPlay.Statistics = statistics;
|
||||
perfectPlay.MaximumStatistics = statistics;
|
||||
|
||||
// calculate total score
|
||||
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
|
||||
scoreProcessor.Mods.Value = perfectPlay.Mods;
|
||||
perfectPlay.TotalScore = scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay);
|
||||
scoreProcessor.ApplyBeatmap(playableBeatmap);
|
||||
perfectPlay.TotalScore = scoreProcessor.MaximumTotalScore;
|
||||
|
||||
// compute rank achieved
|
||||
// default to SS, then adjust the rank with mods
|
||||
|
@ -64,6 +64,11 @@ namespace osu.Game.Rulesets.Judgements
|
||||
/// </summary>
|
||||
public int ComboAtJudgement { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The combo after this <see cref="JudgementResult"/> occurred.
|
||||
/// </summary>
|
||||
public int ComboAfterJudgement { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The highest combo achieved prior to this <see cref="JudgementResult"/> occurring.
|
||||
/// </summary>
|
||||
|
@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
=> AffectsCombo(result) && !IsHit(result);
|
||||
|
||||
/// <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>
|
||||
public static bool AffectsCombo(this HitResult result)
|
||||
{
|
||||
|
@ -4,11 +4,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Linq;
|
||||
using MessagePack;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Localisation;
|
||||
@ -22,6 +21,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
public partial class ScoreProcessor : JudgementProcessor
|
||||
{
|
||||
public const double MAX_SCORE = 1000000;
|
||||
|
||||
private const double accuracy_cutoff_x = 1;
|
||||
private const double accuracy_cutoff_s = 0.95;
|
||||
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_d = 0;
|
||||
|
||||
private const double max_score = 1000000;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="ScoreProcessor"/> was reset from a replay frame.
|
||||
/// </summary>
|
||||
@ -78,39 +77,72 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
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>
|
||||
/// The <see cref="HitEvent"/>s collected during gameplay thus far.
|
||||
/// Intended for use with various statistics displays.
|
||||
/// </summary>
|
||||
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>
|
||||
/// The ruleset this score processor is valid for.
|
||||
/// </summary>
|
||||
public readonly Ruleset Ruleset;
|
||||
|
||||
private readonly double accuracyPortion;
|
||||
private readonly double comboPortion;
|
||||
/// <summary>
|
||||
/// 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
|
||||
{
|
||||
@ -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 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 HitObject? lastHitObject;
|
||||
|
||||
private double scoreMultiplier = 1;
|
||||
|
||||
public ScoreProcessor(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);
|
||||
Accuracy.ValueChanged += accuracy =>
|
||||
{
|
||||
@ -172,7 +175,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
Rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue);
|
||||
};
|
||||
|
||||
Mode.ValueChanged += _ => updateScore();
|
||||
Mods.ValueChanged += mods =>
|
||||
{
|
||||
scoreMultiplier = 1;
|
||||
@ -200,10 +202,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
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())
|
||||
return;
|
||||
|
||||
@ -212,8 +210,21 @@ namespace osu.Game.Rulesets.Scoring
|
||||
else if (result.Type.BreaksCombo())
|
||||
Combo.Value = 0;
|
||||
|
||||
applyResult(result.Type, ref currentScoringValues);
|
||||
currentScoringValues.MaxCombo = HighestCombo.Value;
|
||||
result.ComboAfterJudgement = Combo.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));
|
||||
lastHitObject = result.HitObject;
|
||||
@ -221,20 +232,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
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>
|
||||
/// Creates the <see cref="HitEvent"/> that describes a <see cref="JudgementResult"/>.
|
||||
/// </summary>
|
||||
@ -253,15 +250,22 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
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())
|
||||
return;
|
||||
|
||||
revertResult(result.Type, ref currentScoringValues);
|
||||
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);
|
||||
|
||||
RemoveScoreChange(result);
|
||||
|
||||
Debug.Assert(hitEvents.Count > 0);
|
||||
lastHitObject = hitEvents[^1].LastHitObject;
|
||||
@ -270,110 +274,35 @@ namespace osu.Game.Rulesets.Scoring
|
||||
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())
|
||||
scoringValues.BonusScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
|
||||
else
|
||||
scoringValues.BaseScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
|
||||
|
||||
if (result.IsBasic())
|
||||
scoringValues.CountBasicHitObjects--;
|
||||
protected virtual void RemoveScoreChange(JudgementResult result)
|
||||
{
|
||||
}
|
||||
|
||||
private void updateScore()
|
||||
{
|
||||
Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1;
|
||||
MinimumAccuracy.Value = maximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / maximumScoringValues.BaseScore : 0;
|
||||
MaximumAccuracy.Value = maximumScoringValues.BaseScore > 0
|
||||
? (double)(currentScoringValues.BaseScore + (maximumScoringValues.BaseScore - currentMaximumScoringValues.BaseScore)) / maximumScoringValues.BaseScore
|
||||
: 1;
|
||||
TotalScore.Value = computeScore(Mode.Value, currentScoringValues, maximumScoringValues);
|
||||
Accuracy.Value = currentMaximumBaseScore > 0 ? currentBaseScore / currentMaximumBaseScore : 1;
|
||||
MinimumAccuracy.Value = maximumBaseScore > 0 ? currentBaseScore / maximumBaseScore : 0;
|
||||
MaximumAccuracy.Value = maximumBaseScore > 0 ? (currentBaseScore + (maximumBaseScore - currentMaximumBaseScore)) / maximumBaseScore : 1;
|
||||
|
||||
double comboProgress = maximumComboPortion > 0 ? currentComboPortion / maximumComboPortion : 1;
|
||||
double accuracyProcess = maximumAccuracyJudgementCount > 0 ? (double)currentAccuracyJudgementCount / maximumAccuracyJudgementCount : 1;
|
||||
|
||||
TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion) * scoreMultiplier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
protected virtual double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||
{
|
||||
if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||
throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
|
||||
|
||||
// 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);
|
||||
}
|
||||
return 700000 * comboProgress +
|
||||
300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress +
|
||||
bonusPortion;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -389,16 +318,24 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
if (storeResults)
|
||||
{
|
||||
maximumScoringValues = currentScoringValues;
|
||||
maximumBaseScore = currentBaseScore;
|
||||
|
||||
maximumComboPortion = currentComboPortion;
|
||||
maximumAccuracyJudgementCount = currentAccuracyJudgementCount;
|
||||
|
||||
maximumResultCounts.Clear();
|
||||
maximumResultCounts.AddRange(scoreResultCounts);
|
||||
|
||||
MaximumTotalScore = TotalScore.Value;
|
||||
}
|
||||
|
||||
scoreResultCounts.Clear();
|
||||
|
||||
currentScoringValues = default;
|
||||
currentMaximumScoringValues = default;
|
||||
currentBaseScore = 0;
|
||||
currentMaximumBaseScore = 0;
|
||||
currentAccuracyJudgementCount = 0;
|
||||
currentComboPortion = 0;
|
||||
currentBonusPortion = 0;
|
||||
|
||||
TotalScore.Value = 0;
|
||||
Accuracy.Value = 1;
|
||||
@ -428,7 +365,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result);
|
||||
|
||||
// Populate total score after everything else.
|
||||
score.TotalScore = ComputeScore(ScoringMode.Standardised, score);
|
||||
score.TotalScore = TotalScore.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -452,126 +389,36 @@ namespace osu.Game.Rulesets.Scoring
|
||||
if (frame.Header == null)
|
||||
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;
|
||||
HighestCombo.Value = frame.Header.MaxCombo;
|
||||
TotalScore.Value = frame.Header.TotalScore;
|
||||
|
||||
scoreResultCounts.Clear();
|
||||
scoreResultCounts.AddRange(frame.Header.Statistics);
|
||||
|
||||
SetScoreProcessorStatistics(frame.Header.ScoreProcessorStatistics);
|
||||
|
||||
updateScore();
|
||||
|
||||
OnResetFromReplayFrame?.Invoke();
|
||||
}
|
||||
|
||||
#region ScoringValue extraction
|
||||
|
||||
/// <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)
|
||||
public ScoreProcessorStatistics GetScoreProcessorStatistics() => new ScoreProcessorStatistics
|
||||
{
|
||||
extractScoringValues(scoreInfo.Statistics, out current, out maximum);
|
||||
current.MaxCombo = scoreInfo.MaxCombo;
|
||||
MaximumBaseScore = currentMaximumBaseScore,
|
||||
BaseScore = currentBaseScore,
|
||||
AccuracyJudgementCount = currentAccuracyJudgementCount,
|
||||
ComboPortion = currentComboPortion,
|
||||
BonusPortion = currentBonusPortion
|
||||
};
|
||||
|
||||
if (scoreInfo.MaximumStatistics.Count > 0)
|
||||
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)
|
||||
public void SetScoreProcessorStatistics(ScoreProcessorStatistics statistics)
|
||||
{
|
||||
current = default;
|
||||
maximum = default;
|
||||
|
||||
foreach ((HitResult result, int count) in statistics)
|
||||
{
|
||||
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();
|
||||
currentMaximumBaseScore = statistics.MaximumBaseScore;
|
||||
currentBaseScore = statistics.BaseScore;
|
||||
currentAccuracyJudgementCount = statistics.AccuracyJudgementCount;
|
||||
currentComboPortion = statistics.ComboPortion;
|
||||
currentBonusPortion = statistics.BonusPortion;
|
||||
}
|
||||
|
||||
#region Static helper methods
|
||||
@ -630,30 +477,10 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Stores the required scoring data that fulfils the minimum requirements for a <see cref="ScoreProcessor"/> to calculate score.
|
||||
/// </summary>
|
||||
private struct ScoringValues
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
/// <summary>
|
||||
/// The sum of all "basic" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBasic"/> and <see cref="Judgement.ToNumericResult"/>.
|
||||
/// </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;
|
||||
base.Dispose(isDisposing);
|
||||
hitEvents.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@ -665,4 +492,46 @@ namespace osu.Game.Rulesets.Scoring
|
||||
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.ClassicScoreDisplay))]
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,9 @@ namespace osu.Game.Scoring
|
||||
{
|
||||
IUser User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The standardised total score.
|
||||
/// </summary>
|
||||
long TotalScore { get; }
|
||||
|
||||
int MaxCombo { get; }
|
||||
|
@ -3,13 +3,72 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Scoring.Legacy
|
||||
{
|
||||
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)
|
||||
{
|
||||
switch (scoreInfo.Ruleset.OnlineID)
|
||||
|
@ -20,6 +20,7 @@ using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
|
||||
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>
|
||||
/// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns>
|
||||
public IEnumerable<ScoreInfo> OrderByTotalScore(IEnumerable<ScoreInfo> scores)
|
||||
=> scores.OrderByDescending(s => GetTotalScore(s))
|
||||
=> scores.OrderByDescending(s => s.TotalScore)
|
||||
.ThenBy(s => s.OnlineID)
|
||||
// Local scores may not have an online ID. Fall back to date in these cases.
|
||||
.ThenBy(s => s.Date);
|
||||
@ -87,7 +88,7 @@ namespace osu.Game.Scoring
|
||||
/// </remarks>
|
||||
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
|
||||
/// <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>
|
||||
/// 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>
|
||||
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>
|
||||
/// Retrieves the maximum achievable combo for the provided score.
|
||||
/// </summary>
|
||||
@ -136,12 +118,11 @@ namespace osu.Game.Scoring
|
||||
/// Creates a new <see cref="TotalScoreBindable"/>.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager, OsuConfigManager configManager)
|
||||
public TotalScoreBindable(ScoreInfo score, OsuConfigManager configManager)
|
||||
{
|
||||
configManager?.BindWith(OsuSetting.ScoreDisplayMode, scoringMode);
|
||||
scoringMode.BindValueChanged(mode => Value = scoreManager.GetTotalScore(score, mode.NewValue), true);
|
||||
scoringMode.BindValueChanged(mode => Value = score.GetDisplayScore(mode.NewValue), true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,14 +6,12 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
@ -1,18 +1,17 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osu.Game.Utils;
|
||||
@ -48,7 +47,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
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 BindableDouble Accuracy { get; } = new BindableDouble(1);
|
||||
@ -56,6 +55,13 @@ namespace osu.Game.Screens.Play.HUD
|
||||
public BindableBool HasQuit { get; } = new BindableBool();
|
||||
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? TextColour { get; set; }
|
||||
@ -82,40 +88,43 @@ namespace osu.Game.Screens.Play.HUD
|
||||
}
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
public IUser User { get; }
|
||||
public IUser? User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this score is the local user or a replay player (and should be focused / always visible).
|
||||
/// </summary>
|
||||
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>
|
||||
/// Creates a new <see cref="GameplayLeaderboardScore"/>.
|
||||
/// </summary>
|
||||
/// <param name="user">The score's 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;
|
||||
Tracked = tracked;
|
||||
|
||||
AutoSizeAxes = Axes.X;
|
||||
Height = PANEL_HEIGHT;
|
||||
|
||||
GetDisplayScore = _ => TotalScore.Value;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
private void load(OsuColour colours, OsuConfigManager osuConfigManager)
|
||||
{
|
||||
Container avatarContainer;
|
||||
|
||||
@ -234,7 +243,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
Origin = Anchor.CentreLeft,
|
||||
Colour = Color4.White,
|
||||
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
|
||||
Text = User?.Username,
|
||||
Text = User?.Username ?? string.Empty,
|
||||
Truncate = true,
|
||||
Shadow = false,
|
||||
}
|
||||
@ -286,7 +295,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
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 =>
|
||||
{
|
||||
@ -313,6 +324,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
private void updateScore() => scoreText.Text = (getDisplayScoreFunction?.Invoke(scoreDisplayMode.Value) ?? TotalScore.Value).ToString("N0");
|
||||
|
||||
private void changeExpandedState(ValueChangedEvent<bool> expanded)
|
||||
{
|
||||
if (expanded.NewValue)
|
||||
|
@ -1,20 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public abstract partial class GameplayScoreCounter : ScoreCounter
|
||||
{
|
||||
private Bindable<ScoringMode> scoreDisplayMode;
|
||||
private Bindable<ScoringMode> scoreDisplayMode = null!;
|
||||
|
||||
private Bindable<long> totalScoreBindable = null!;
|
||||
|
||||
protected GameplayScoreCounter()
|
||||
: base(6)
|
||||
@ -24,6 +25,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config, ScoreProcessor scoreProcessor)
|
||||
{
|
||||
totalScoreBindable = scoreProcessor.TotalScore.GetBoundCopy();
|
||||
totalScoreBindable.BindValueChanged(_ => updateDisplayScore());
|
||||
|
||||
scoreDisplayMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
|
||||
scoreDisplayMode.BindValueChanged(scoreMode =>
|
||||
{
|
||||
@ -40,9 +44,11 @@ namespace osu.Game.Screens.Play.HUD
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(scoreMode));
|
||||
}
|
||||
|
||||
updateDisplayScore();
|
||||
}, true);
|
||||
|
||||
Current.BindTo(scoreProcessor.TotalScore);
|
||||
void updateDisplayScore() => Current.Value = scoreProcessor.GetDisplayScore(scoreDisplayMode.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
var trackedUser = UserScores[user.Id];
|
||||
|
||||
var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id);
|
||||
leaderboardScore.GetDisplayScore = trackedUser.ScoreProcessor.GetDisplayScore;
|
||||
leaderboardScore.Accuracy.BindTo(trackedUser.ScoreProcessor.Accuracy);
|
||||
leaderboardScore.TotalScore.BindTo(trackedUser.ScoreProcessor.TotalScore);
|
||||
leaderboardScore.Combo.BindTo(trackedUser.ScoreProcessor.Combo);
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -10,6 +9,7 @@ using osu.Game.Configuration;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Users;
|
||||
|
||||
@ -27,15 +27,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
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]
|
||||
private ScoreProcessor scoreProcessor { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the leaderboard should be visible regardless of the configuration value.
|
||||
/// This is true by default, but can be changed.
|
||||
@ -70,7 +64,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
private void showScores()
|
||||
{
|
||||
Clear();
|
||||
scoreBindables.Clear();
|
||||
|
||||
if (!Scores.Any())
|
||||
return;
|
||||
@ -79,12 +72,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
var score = Add(s.User, false);
|
||||
|
||||
var bindableTotal = scoreManager.GetBindableTotalScore(s);
|
||||
|
||||
// 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.GetDisplayScore = s.GetDisplayScore;
|
||||
score.TotalScore.Value = s.TotalScore;
|
||||
score.Accuracy.Value = s.Accuracy;
|
||||
score.Combo.Value = s.MaxCombo;
|
||||
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);
|
||||
|
||||
local.GetDisplayScore = scoreProcessor.GetDisplayScore;
|
||||
local.TotalScore.BindTarget = scoreProcessor.TotalScore;
|
||||
local.Accuracy.BindTarget = scoreProcessor.Accuracy;
|
||||
local.Combo.BindTarget = scoreProcessor.HighestCombo;
|
||||
|
@ -237,9 +237,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
dependencies.CacheAs(HealthProcessor);
|
||||
|
||||
if (!ScoreProcessor.Mode.Disabled)
|
||||
config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
|
||||
|
||||
InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime);
|
||||
|
||||
AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));
|
||||
|
@ -9,7 +9,6 @@ using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -67,9 +66,6 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; }
|
||||
|
||||
private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource();
|
||||
private readonly Flow flow;
|
||||
private readonly Scroll scroll;
|
||||
@ -149,7 +145,7 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
var score = trackingContainer.Panel.Score;
|
||||
|
||||
flow.SetLayoutPosition(trackingContainer, scoreManager.GetTotalScore(score));
|
||||
flow.SetLayoutPosition(trackingContainer, score.TotalScore);
|
||||
|
||||
trackingContainer.Show();
|
||||
|
||||
|
@ -12,7 +12,9 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Replays.Legacy;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Spectator
|
||||
@ -44,6 +46,9 @@ namespace osu.Game.Tests.Visual.Spectator
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesetStore { get; set; } = null!;
|
||||
|
||||
public TestSpectatorClient()
|
||||
{
|
||||
OnNewFrames += (i, bundle) => lastReceivedUserFrames[i] = bundle.Frames[^1];
|
||||
@ -119,7 +124,7 @@ namespace osu.Game.Tests.Visual.Spectator
|
||||
if (frames.Count == 0)
|
||||
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);
|
||||
|
||||
frames.Clear();
|
||||
|
Loading…
Reference in New Issue
Block a user