// 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 osu.Framework.Bindables; using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Objects { /// <summary> /// A <see cref="LifetimeEntry"/> that stores the lifetime for a <see cref="HitObject"/>. /// </summary> public class HitObjectLifetimeEntry : LifetimeEntry { /// <summary> /// The <see cref="HitObject"/>. /// </summary> public readonly HitObject HitObject; /// <summary> /// The list of <see cref="HitObjectLifetimeEntry"/> for the <see cref="HitObject"/>'s nested objects (if any). /// </summary> public List<HitObjectLifetimeEntry> NestedEntries { get; internal set; } = new List<HitObjectLifetimeEntry>(); /// <summary> /// The result that <see cref="HitObject"/> was judged with. /// This is set by the accompanying <see cref="DrawableHitObject"/>, and reused when required for rewinding. /// </summary> internal JudgementResult? Result; /// <summary> /// Whether <see cref="HitObject"/> has been judged. /// Note: This does NOT include nested hitobjects. /// </summary> public bool Judged => Result?.HasResult ?? false; /// <summary> /// Whether <see cref="HitObject"/> and all of its nested objects have been judged. /// </summary> public bool AllJudged { get { if (!Judged) return false; foreach (var entry in NestedEntries) { if (!entry.AllJudged) return false; } return true; } } private readonly IBindable<double> startTimeBindable = new BindableDouble(); internal event Action? RevertResult; /// <summary> /// Creates a new <see cref="HitObjectLifetimeEntry"/>. /// </summary> /// <param name="hitObject">The <see cref="HitObject"/> to store the lifetime of.</param> public HitObjectLifetimeEntry(HitObject hitObject) { HitObject = hitObject; startTimeBindable.BindTo(HitObject.StartTimeBindable); startTimeBindable.BindValueChanged(_ => SetInitialLifetime(), true); // Subscribe to this event before the DrawableHitObject so that the local callback is invoked before the entry is re-applied as a result of DefaultsApplied. // This way, the DrawableHitObject can use OnApply() to overwrite the LifetimeStart that was set inside setInitialLifetime(). HitObject.DefaultsApplied += _ => SetInitialLifetime(); } // The lifetime, as set by the hitobject. private double realLifetimeStart = double.MinValue; private double realLifetimeEnd = double.MaxValue; // This method is called even if `start == LifetimeStart` when `KeepAlive` is true (necessary to update `realLifetimeStart`). protected override void SetLifetimeStart(double start) { realLifetimeStart = start; if (!keepAlive) base.SetLifetimeStart(start); } protected override void SetLifetimeEnd(double end) { realLifetimeEnd = end; if (!keepAlive) base.SetLifetimeEnd(end); } private bool keepAlive; /// <summary> /// Whether the <see cref="HitObject"/> should be kept always alive. /// </summary> internal bool KeepAlive { set { if (keepAlive == value) return; keepAlive = value; if (keepAlive) SetLifetime(double.MinValue, double.MaxValue); else SetLifetime(realLifetimeStart, realLifetimeEnd); } } /// <summary> /// A safe offset prior to the start time of <see cref="HitObject"/> at which it may begin displaying contents. /// By default, <see cref="HitObject"/>s are assumed to display their contents within 10 seconds prior to their start time. /// </summary> /// <remarks> /// This is only used as an optimisation to delay the initial application of the <see cref="HitObject"/> to a <see cref="DrawableHitObject"/>. /// A more accurate <see cref="LifetimeEntry.LifetimeStart"/> should be set on the hit object application, for further optimisation. /// </remarks> protected virtual double InitialLifetimeOffset => 10000; /// <summary> /// Set <see cref="LifetimeEntry.LifetimeStart"/> using <see cref="InitialLifetimeOffset"/>. /// </summary> internal void SetInitialLifetime() => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; internal void OnRevertResult() => RevertResult?.Invoke(); } }