mirror of
https://github.com/ppy/osu.git
synced 2026-06-02 06:09:55 +08:00
e3eeb761aa
- Related to https://github.com/ppy/osu/issues/37818, but of no material help to it at this point (too late for that) As noted in https://github.com/ppy/osu/pull/37845#discussion_r3297203361. Upon comparison of replays recorded by the client and by the server the affected fields are: total score without mods, and the list of user pauses. Additionally, the date of setting the score may differ - server-side it seems to be written with UTC+0 while client-side it's written using the local timezone offset. Not really interested in fixing that last issue at this time. Also included is an intentionally loud disclaimer in `LegacyScoreEncoder` to tread with caution when treating the class. Not sure it'll help, and it's a bit late for it as pretty much every single versioning primitive has been ravaged to the brink of unusability, but maybe it'll help someone in the future. This also cleans up an unnecessary nullable on `FrameHeader.Mods` (added in https://github.com/ppy/osu/pull/30137). This change can be only done if users on releases earlier than 2024.1023.0 can no longer connect to spectator server. I leave it to reviewers to determine this as I have no visibility over current spectator server configuration. Inspecting the `osu_builds` table may help confirm this. If it provokes unease, I can back this change out.
203 lines
8.8 KiB
C#
203 lines
8.8 KiB
C#
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using NUnit.Framework;
|
|
using osu.Framework.Testing;
|
|
using osu.Framework.Utils;
|
|
using osu.Game.Beatmaps;
|
|
using osu.Game.Online.Spectator;
|
|
using osu.Game.Rulesets.Judgements;
|
|
using osu.Game.Rulesets.Objects;
|
|
using osu.Game.Rulesets.Osu;
|
|
using osu.Game.Rulesets.Osu.Objects;
|
|
using osu.Game.Rulesets.Osu.Replays;
|
|
using osu.Game.Rulesets.Scoring;
|
|
using osu.Game.Scoring;
|
|
using osu.Game.Tests.Visual;
|
|
|
|
namespace osu.Game.Tests.Gameplay
|
|
{
|
|
[HeadlessTest]
|
|
public partial class TestSceneScoreProcessor : OsuTestScene
|
|
{
|
|
[Test]
|
|
public void TestNoScoreIncreaseFromMiss()
|
|
{
|
|
var beatmap = new Beatmap<HitObject> { HitObjects = { new HitObject() } };
|
|
|
|
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
|
scoreProcessor.ApplyBeatmap(beatmap);
|
|
|
|
// Apply a miss judgement
|
|
scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new TestJudgement()) { Type = HitResult.Miss });
|
|
|
|
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0));
|
|
}
|
|
|
|
[Test]
|
|
public void TestOnlyBonusScore()
|
|
{
|
|
var beatmap = new Beatmap<HitObject> { HitObjects = { new HitObject() } };
|
|
|
|
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
|
scoreProcessor.ApplyBeatmap(beatmap);
|
|
|
|
// Apply a judgement
|
|
scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new TestJudgement(HitResult.LargeBonus)) { Type = HitResult.LargeBonus });
|
|
|
|
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(scoreProcessor.GetBaseScoreForResult(HitResult.LargeBonus)));
|
|
}
|
|
|
|
[Test]
|
|
public void TestResetFromReplayFrame()
|
|
{
|
|
var beatmap = new Beatmap<HitObject> { HitObjects = { new HitCircle() } };
|
|
|
|
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
|
scoreProcessor.ApplyBeatmap(beatmap);
|
|
|
|
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TestJudgement(HitResult.Great)) { Type = HitResult.Great });
|
|
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
|
|
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
|
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
|
|
|
|
// No header shouldn't cause any change
|
|
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame());
|
|
|
|
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
|
|
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
|
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
|
|
|
|
// Reset with a miss instead.
|
|
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
|
|
{
|
|
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, [], 0, [])
|
|
});
|
|
|
|
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, 0, new Dictionary<HitResult, int>(), new ScoreProcessorStatistics
|
|
{
|
|
MaximumBaseScore = 0,
|
|
BaseScore = 0,
|
|
AccuracyJudgementCount = 0,
|
|
ComboPortion = 0,
|
|
BonusPortion = 0
|
|
}, DateTimeOffset.Now, [], 0, [])
|
|
});
|
|
|
|
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]
|
|
public void TestFailScore()
|
|
{
|
|
var beatmap = new Beatmap<HitObject>
|
|
{
|
|
HitObjects =
|
|
{
|
|
new TestHitObject(),
|
|
new TestHitObject(HitResult.LargeTickHit),
|
|
new TestHitObject(HitResult.SmallTickHit),
|
|
new TestHitObject(HitResult.SmallBonus),
|
|
new TestHitObject(),
|
|
new TestHitObject(HitResult.LargeTickHit),
|
|
new TestHitObject(HitResult.SmallTickHit),
|
|
new TestHitObject(HitResult.LargeBonus),
|
|
}
|
|
};
|
|
|
|
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
|
scoreProcessor.ApplyBeatmap(beatmap);
|
|
|
|
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].Judgement) { Type = HitResult.Ok });
|
|
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].Judgement) { Type = HitResult.LargeTickHit });
|
|
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], beatmap.HitObjects[2].Judgement) { Type = HitResult.SmallTickMiss });
|
|
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], beatmap.HitObjects[3].Judgement) { Type = HitResult.SmallBonus });
|
|
|
|
var score = new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo };
|
|
scoreProcessor.FailScore(score);
|
|
|
|
Assert.That(score.Rank, Is.EqualTo(ScoreRank.F));
|
|
Assert.That(score.Passed, Is.False);
|
|
Assert.That(score.Statistics.Sum(kvp => kvp.Value), Is.EqualTo(4));
|
|
Assert.That(score.MaximumStatistics.Sum(kvp => kvp.Value), Is.EqualTo(8));
|
|
|
|
Assert.That(score.Statistics[HitResult.Ok], Is.EqualTo(1));
|
|
Assert.That(score.Statistics[HitResult.LargeTickHit], Is.EqualTo(1));
|
|
Assert.That(score.Statistics[HitResult.SmallTickMiss], Is.EqualTo(1));
|
|
Assert.That(score.Statistics[HitResult.SmallBonus], Is.EqualTo(1));
|
|
|
|
Assert.That(score.MaximumStatistics[HitResult.Perfect], Is.EqualTo(2));
|
|
Assert.That(score.MaximumStatistics[HitResult.LargeTickHit], Is.EqualTo(2));
|
|
Assert.That(score.MaximumStatistics[HitResult.SmallTickHit], Is.EqualTo(2));
|
|
Assert.That(score.MaximumStatistics[HitResult.SmallBonus], Is.EqualTo(1));
|
|
Assert.That(score.MaximumStatistics[HitResult.LargeBonus], Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void TestAccuracyModes()
|
|
{
|
|
var beatmap = new Beatmap<HitObject>
|
|
{
|
|
HitObjects = Enumerable.Range(0, 4).Select(_ => new TestHitObject(HitResult.Great)).ToList<HitObject>()
|
|
};
|
|
|
|
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
|
scoreProcessor.ApplyBeatmap(beatmap);
|
|
|
|
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
|
|
Assert.That(scoreProcessor.MinimumAccuracy.Value, Is.EqualTo(0));
|
|
Assert.That(scoreProcessor.MaximumAccuracy.Value, Is.EqualTo(1));
|
|
|
|
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].Judgement) { Type = HitResult.Ok });
|
|
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].Judgement) { Type = HitResult.Great });
|
|
|
|
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo((double)(100 + 300) / (2 * 300)).Within(Precision.DOUBLE_EPSILON));
|
|
Assert.That(scoreProcessor.MinimumAccuracy.Value, Is.EqualTo((double)(100 + 300) / (4 * 300)).Within(Precision.DOUBLE_EPSILON));
|
|
Assert.That(scoreProcessor.MaximumAccuracy.Value, Is.EqualTo((double)(100 + 3 * 300) / (4 * 300)).Within(Precision.DOUBLE_EPSILON));
|
|
}
|
|
|
|
private class TestJudgement : Judgement
|
|
{
|
|
public override HitResult MaxResult { get; }
|
|
|
|
public TestJudgement(HitResult maxResult = HitResult.Perfect)
|
|
{
|
|
MaxResult = maxResult;
|
|
}
|
|
}
|
|
|
|
private class TestHitObject : HitObject
|
|
{
|
|
private readonly HitResult maxResult;
|
|
|
|
public TestHitObject(HitResult maxResult = HitResult.Perfect)
|
|
{
|
|
this.maxResult = maxResult;
|
|
}
|
|
|
|
public override Judgement CreateJudgement() => new TestJudgement(maxResult);
|
|
}
|
|
}
|
|
}
|