// 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 osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; 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 { public abstract partial class JudgementProcessor : Component { /// /// Invoked when a new judgement has occurred. This occurs after the judgement has been processed by this . /// public event Action? NewJudgement; /// /// Invoked when a judgement is reverted, usually due to rewinding gameplay. /// public event Action? JudgementReverted; /// /// The maximum number of hits that can be judged. /// protected int MaxHits { get; private set; } /// /// Whether is currently running. /// protected bool IsSimulating { get; private set; } /// /// The total number of judged s at the current point in time. /// public int JudgedHits { get; private set; } private JudgementResult? lastAppliedResult; private readonly BindableBool hasCompleted = new BindableBool(); /// /// Whether all s have been processed. /// public IBindable HasCompleted => hasCompleted; /// /// Applies a to this . /// /// The to read properties from. public virtual void ApplyBeatmap(IBeatmap beatmap) { Reset(false); SimulateAutoplay(beatmap); Reset(true); } /// /// Applies the score change of a to this . /// /// The to apply. public void ApplyResult(JudgementResult result) { #pragma warning disable CS0618 if (result.Type == HitResult.LegacyComboIncrease) throw new ArgumentException(@$"A {nameof(HitResult.LegacyComboIncrease)} hit result cannot be applied."); #pragma warning restore CS0618 JudgedHits++; lastAppliedResult = result; ApplyResultInternal(result); NewJudgement?.Invoke(result); } /// /// Reverts the score change of a that was applied to this . /// /// The judgement scoring result. public void RevertResult(JudgementResult result) { JudgedHits--; RevertResultInternal(result); JudgementReverted?.Invoke(result); } /// /// Applies the score change of a to this . /// /// /// Any changes applied via this method can be reverted via . /// /// The to apply. protected abstract void ApplyResultInternal(JudgementResult result); /// /// Reverts the score change of a that was applied to this via . /// /// The judgement scoring result. protected abstract void RevertResultInternal(JudgementResult result); /// /// Resets this to a default state. /// /// Whether to store the current state of the for future use. protected virtual void Reset(bool storeResults) { if (storeResults) MaxHits = JudgedHits; JudgedHits = 0; } /// /// Reset all statistics based on header information contained within a replay frame. /// /// /// If the provided replay frame does not have any header information, this will be a noop. /// /// The replay frame to read header statistics from. public virtual void ResetFromReplayFrame(ReplayFrame frame) { if (frame.Header == null) return; JudgedHits = 0; foreach ((_, int count) in frame.Header.Statistics) JudgedHits += count; } /// /// Simulates an autoplay of the to determine scoring values. /// /// This provided temporarily. DO NOT USE. /// The to simulate. protected void SimulateAutoplay(IBeatmap beatmap) { IsSimulating = true; foreach (var obj in EnumerateHitObjects(beatmap)) { var judgement = obj.Judgement; var result = CreateResult(obj, judgement); if (result == null) throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); result.Type = GetSimulatedHitResult(judgement); ApplyResult(result); } IsSimulating = false; } /// /// Enumerates all s in the given in the order in which they are to be judged. /// Used in . /// /// /// In Score V2, the score awarded for each object includes a component based on the combo value after the judgement of that object. /// This means that the score is dependent on the order of evaluation of judgements. /// This method is provided so that rulesets can specify custom ordering that is correct for them and matches processing order during actual gameplay. /// protected virtual IEnumerable EnumerateHitObjects(IBeatmap beatmap) => enumerateRecursively(beatmap.HitObjects); private IEnumerable enumerateRecursively(IEnumerable hitObjects) { foreach (var hitObject in hitObjects) { foreach (var nested in enumerateRecursively(hitObject.NestedHitObjects)) yield return nested; yield return hitObject; } } /// /// Creates the that represents the scoring result for a . /// /// The which was judged. /// The that provides the scoring information. protected virtual JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new JudgementResult(hitObject, judgement); /// /// Gets a simulated for a judgement. Used during to simulate a "perfect" play. /// /// The judgement to simulate a for. /// The simulated for the judgement. protected virtual HitResult GetSimulatedHitResult(Judgement judgement) => judgement.MaxResult; protected override void Update() { base.Update(); hasCompleted.Value = JudgedHits == MaxHits && (JudgedHits == 0 // Last applied result is guaranteed to be non-null when JudgedHits > 0. || lastAppliedResult.AsNonNull().TimeAbsolute < Clock.CurrentTime); } } }