diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index 432e3df95e..bcaa70d062 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -1,11 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Testing; 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.Tests.Visual; @@ -42,6 +48,37 @@ namespace osu.Game.Tests.Gameplay Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(Judgement.LARGE_BONUS_SCORE)); } + [Test] + public void TestResetFromReplayFrame() + { + var beatmap = new Beatmap { HitObjects = { new HitCircle() } }; + + var scoreProcessor = new ScoreProcessor(); + 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)); + + // Reset with a miss instead. + scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame + { + Header = new FrameHeader(0, 0, 0, new Dictionary { { HitResult.Miss, 1 } }, DateTimeOffset.Now) + }); + + Assert.That(scoreProcessor.TotalScore.Value, Is.Zero); + Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); + + // Reset with no judged hit. + scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame + { + Header = new FrameHeader(0, 0, 0, new Dictionary(), DateTimeOffset.Now) + }); + + Assert.That(scoreProcessor.TotalScore.Value, Is.Zero); + Assert.That(scoreProcessor.JudgedHits, Is.Zero); + } + private class TestJudgement : Judgement { public override HitResult MaxResult { get; } diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index c3c4a2c949..f38155240e 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Scoring { @@ -107,6 +108,17 @@ namespace osu.Game.Rulesets.Scoring JudgedHits = 0; } + public virtual void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame) + { + if (frame.Header == null) + return; + + JudgedHits = 0; + + foreach ((_, int count) in frame.Header.Statistics) + JudgedHits += count; + } + /// /// Creates the that represents the scoring result for a . /// diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index fc5e982521..37ee3771df 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -7,9 +7,11 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Utils; +using osu.Game.Extensions; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Replays; using osu.Game.Scoring; namespace osu.Game.Rulesets.Scoring @@ -18,6 +20,11 @@ namespace osu.Game.Rulesets.Scoring { private const double max_score = 1000000; + /// + /// Invoked when this was reset from a replay frame. + /// + public event Action OnResetFromReplayFrame; + /// /// The current total score. /// @@ -329,12 +336,6 @@ namespace osu.Game.Rulesets.Scoring HighestCombo.Value = 0; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - hitEvents.Clear(); - } - /// /// Retrieve a score populated with data for the current play this processor is responsible for. /// @@ -351,6 +352,70 @@ namespace osu.Game.Rulesets.Scoring score.HitEvents = hitEvents; } + + /// + /// Maximum for a normal hit (i.e. not tick/bonus) for this ruleset. Only populated via . + /// + private HitResult? maxNormalResult; + + public override void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame) + { + base.ResetFromReplayFrame(ruleset, frame); + + if (frame.Header == null) + return; + + baseScore = 0; + rollingMaxBaseScore = 0; + HighestCombo.Value = frame.Header.MaxCombo; + + foreach ((HitResult result, int count) in frame.Header.Statistics) + { + // Bonus scores are counted separately directly from the statistics dictionary later on. + if (!result.IsScorable() || result.IsBonus()) + continue; + + // 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 = maxNormalResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result; + break; + } + + for (int i = 0; i < count; i++) + { + baseScore += Judgement.ToNumericResult(result); + rollingMaxBaseScore += Judgement.ToNumericResult(maxResult); + } + } + + scoreResultCounts.Clear(); + scoreResultCounts.AddRange(frame.Header.Statistics); + + updateScore(); + + OnResetFromReplayFrame?.Invoke(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + hitEvents.Clear(); + } } public enum ScoringMode diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 370c99ffaf..050ec82071 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -16,6 +16,7 @@ using osu.Game.Configuration; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Input.Handlers; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using static osu.Game.Input.Handlers.ReplayInputHandler; @@ -24,6 +25,11 @@ namespace osu.Game.Rulesets.UI public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler, IHasRecordingHandler where T : struct { + private readonly Ruleset ruleset; + + [Resolved(CanBeNull = true)] + private ScoreProcessor scoreProcessor { get; set; } + private ReplayRecorder recorder; public ReplayRecorder Recorder @@ -51,6 +57,8 @@ namespace osu.Game.Rulesets.UI protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) { + this.ruleset = ruleset.CreateInstance(); + InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique) .WithChild(content = new Container { RelativeSizeAxes = Axes.Both }); @@ -66,17 +74,23 @@ namespace osu.Game.Rulesets.UI public override void HandleInputStateChange(InputStateChangeEvent inputStateChange) { - if (inputStateChange is ReplayStateChangeEvent replayStateChanged) + switch (inputStateChange) { - foreach (var action in replayStateChanged.ReleasedActions) - KeyBindingContainer.TriggerReleased(action); + case ReplayStateChangeEvent stateChangeEvent: + foreach (var action in stateChangeEvent.ReleasedActions) + KeyBindingContainer.TriggerReleased(action); - foreach (var action in replayStateChanged.PressedActions) - KeyBindingContainer.TriggerPressed(action); - } - else - { - base.HandleInputStateChange(inputStateChange); + foreach (var action in stateChangeEvent.PressedActions) + KeyBindingContainer.TriggerPressed(action); + break; + + case ReplayStatisticsFrameEvent statisticsStateChangeEvent: + scoreProcessor?.ResetFromReplayFrame(ruleset, statisticsStateChangeEvent.Frame); + break; + + default: + base.HandleInputStateChange(inputStateChange); + break; } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index cfca2d0a3d..e0fcca4eb7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -165,6 +165,7 @@ namespace osu.Game.Screens.Play PrepareReplay(); ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo); + ScoreProcessor.OnResetFromReplayFrame += () => ScoreProcessor.PopulateScore(Score.ScoreInfo); gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true); }