// 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 System.Linq; using System.Threading; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Objects { /// /// A HitObject describes an object in a Beatmap. /// /// HitObjects may contain more properties for which you should be checking through the IHas* types. /// /// public class HitObject { /// /// A small adjustment to the start time of control points to account for rounding/precision errors. /// private const double control_point_leniency = 1; /// /// Invoked after has completed on this . /// public event Action DefaultsApplied; public readonly Bindable StartTimeBindable = new BindableDouble(); /// /// The time at which the HitObject starts. /// public virtual double StartTime { get => StartTimeBindable.Value; set => StartTimeBindable.Value = value; } public readonly BindableList SamplesBindable = new BindableList(); /// /// The samples to be played when this hit object is hit. /// /// In the case of types, this is the sample of the curve body /// and can be treated as the default samples for the hit object. /// /// public IList Samples { get => SamplesBindable; set { SamplesBindable.Clear(); SamplesBindable.AddRange(value); } } [JsonIgnore] public SampleControlPoint SampleControlPoint; /// /// Whether this is in Kiai time. /// [JsonIgnore] public bool Kiai { get; private set; } /// /// The hit windows for this . /// public HitWindows HitWindows { get; set; } private readonly List nestedHitObjects = new List(); [JsonIgnore] public IReadOnlyList NestedHitObjects => nestedHitObjects; public HitObject() { StartTimeBindable.ValueChanged += time => { double offset = time.NewValue - time.OldValue; foreach (var nested in NestedHitObjects) nested.StartTime += offset; }; } /// /// Applies default values to this HitObject. /// /// The control points. /// The difficulty settings to use. /// The cancellation token. public void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty, CancellationToken cancellationToken = default) { ApplyDefaultsToSelf(controlPointInfo, difficulty); // This is done here since ApplyDefaultsToSelf may be used to determine the end time SampleControlPoint = controlPointInfo.SamplePointAt(this.GetEndTime() + control_point_leniency); nestedHitObjects.Clear(); CreateNestedHitObjects(cancellationToken); if (this is IHasComboInformation hasCombo) { foreach (var n in NestedHitObjects.OfType()) { n.ComboIndexBindable.BindTo(hasCombo.ComboIndexBindable); n.IndexInCurrentComboBindable.BindTo(hasCombo.IndexInCurrentComboBindable); } } nestedHitObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); foreach (var h in nestedHitObjects) h.ApplyDefaults(controlPointInfo, difficulty, cancellationToken); DefaultsApplied?.Invoke(this); } protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { Kiai = controlPointInfo.EffectPointAt(StartTime + control_point_leniency).KiaiMode; HitWindows ??= CreateHitWindows(); HitWindows?.SetDifficulty(difficulty.OverallDifficulty); } protected virtual void CreateNestedHitObjects(CancellationToken cancellationToken) { // ReSharper disable once MethodSupportsCancellation (https://youtrack.jetbrains.com/issue/RIDER-44520) #pragma warning disable 618 CreateNestedHitObjects(); #pragma warning restore 618 } [Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318 protected virtual void CreateNestedHitObjects() { } protected void AddNested(HitObject hitObject) => nestedHitObjects.Add(hitObject); /// /// Creates the that represents the scoring information for this . /// [NotNull] public virtual Judgement CreateJudgement() => new Judgement(); /// /// Creates the for this . /// This can be null to indicate that the has no and timing errors should not be displayed to the user. /// /// This will only be invoked if hasn't been set externally (e.g. from a . /// /// [NotNull] protected virtual HitWindows CreateHitWindows() => new HitWindows(); } public static class HitObjectExtensions { /// /// Returns the end time of this object. /// /// /// This returns the where available, falling back to otherwise. /// /// The object. /// The end time of this object. public static double GetEndTime(this HitObject hitObject) => (hitObject as IHasDuration)?.EndTime ?? hitObject.StartTime; } }