2019-01-24 17:43:03 +09:00
// 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.
2018-04-13 18:19:50 +09:00
2022-06-17 16:37:17 +09:00
#nullable disable
2019-10-18 13:18:41 +09:00
using System ;
2017-12-22 21:42:54 +09:00
using System.Collections.Generic ;
2022-03-14 15:45:43 +09:00
using System.Collections.Immutable ;
2022-10-11 16:31:37 +09:00
using System.Linq ;
2020-05-15 18:07:41 +09:00
using System.Threading ;
2019-09-02 17:48:41 +09:00
using JetBrains.Annotations ;
2017-12-07 14:42:36 +09:00
using Newtonsoft.Json ;
2019-10-03 14:23:48 +09:00
using osu.Framework.Bindables ;
2021-09-20 01:48:33 +09:00
using osu.Framework.Extensions.ListExtensions ;
using osu.Framework.Lists ;
2017-04-06 11:41:16 +09:00
using osu.Game.Audio ;
2017-07-26 13:22:46 +09:00
using osu.Game.Beatmaps ;
2017-05-23 13:55:18 +09:00
using osu.Game.Beatmaps.ControlPoints ;
2018-08-01 21:04:03 +09:00
using osu.Game.Rulesets.Judgements ;
2017-04-21 16:18:34 +09:00
using osu.Game.Rulesets.Objects.Types ;
2019-09-06 15:24:00 +09:00
using osu.Game.Rulesets.Scoring ;
2018-04-13 18:19:50 +09:00
2017-04-18 16:05:58 +09:00
namespace osu.Game.Rulesets.Objects
2016-08-31 12:33:01 +09:00
{
/// <summary>
2017-03-13 19:15:25 +09:00
/// A HitObject describes an object in a Beatmap.
/// <para>
/// HitObjects may contain more properties for which you should be checking through the IHas* types.
/// </para>
2016-08-31 12:33:01 +09:00
/// </summary>
2023-04-26 13:10:57 +02:00
public class HitObject
2016-08-31 12:33:01 +09:00
{
2018-12-04 12:01:30 +09:00
/// <summary>
/// A small adjustment to the start time of control points to account for rounding/precision errors.
/// </summary>
private const double control_point_leniency = 1 ;
2019-10-18 13:18:41 +09:00
/// <summary>
/// Invoked after <see cref="ApplyDefaults"/> has completed on this <see cref="HitObject"/>.
/// </summary>
2020-05-08 18:49:19 +09:00
public event Action < HitObject > DefaultsApplied ;
2019-10-18 13:18:41 +09:00
2020-02-01 22:50:29 +01:00
public readonly Bindable < double > StartTimeBindable = new BindableDouble ( ) ;
2019-10-03 14:23:48 +09:00
2017-03-13 19:15:25 +09:00
/// <summary>
/// The time at which the HitObject starts.
/// </summary>
2019-10-03 14:23:48 +09:00
public virtual double StartTime
{
get = > StartTimeBindable . Value ;
set = > StartTimeBindable . Value = value ;
}
2018-04-13 18:19:50 +09:00
2019-11-08 14:04:57 +09:00
public readonly BindableList < HitSampleInfo > SamplesBindable = new BindableList < HitSampleInfo > ( ) ;
2018-04-13 18:19:50 +09:00
2017-03-13 19:15:25 +09:00
/// <summary>
2017-04-06 11:41:16 +09:00
/// The samples to be played when this hit object is hit.
2017-04-21 18:49:49 +09:00
/// <para>
2017-04-21 20:42:13 +09:00
/// In the case of <see cref="IHasRepeats"/> types, this is the sample of the curve body
2017-04-21 18:49:49 +09:00
/// and can be treated as the default samples for the hit object.
/// </para>
2017-03-13 19:15:25 +09:00
/// </summary>
2019-11-08 14:04:57 +09:00
public IList < HitSampleInfo > Samples
2017-12-25 16:41:18 +09:00
{
2019-11-08 14:04:57 +09:00
get = > SamplesBindable ;
set
{
SamplesBindable . Clear ( ) ;
SamplesBindable . AddRange ( value ) ;
}
2017-12-25 16:41:18 +09:00
}
2018-04-13 18:19:50 +09:00
2022-03-14 15:45:43 +09:00
/// <summary>
/// Any samples which may be used by this hit object that are non-standard.
/// This is used only to preload these samples ahead of time.
/// </summary>
public virtual IList < HitSampleInfo > AuxiliarySamples = > ImmutableList < HitSampleInfo > . Empty ;
2023-04-26 13:10:57 +02:00
/// <summary>
/// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it.
/// DO NOT USE THIS UNLESS 100% SURE.
/// </summary>
public double? LegacyBpmMultiplier { get ; set ; }
2017-09-12 10:01:07 +09:00
/// <summary>
/// Whether this <see cref="HitObject"/> is in Kiai time.
/// </summary>
2017-12-07 14:42:36 +09:00
[JsonIgnore]
2017-09-12 10:01:07 +09:00
public bool Kiai { get ; private set ; }
2018-04-13 18:19:50 +09:00
2018-02-02 18:47:10 +09:00
/// <summary>
2018-02-08 17:38:46 +09:00
/// The hit windows for this <see cref="HitObject"/>.
2018-02-02 18:47:10 +09:00
/// </summary>
2020-10-07 13:45:41 +09:00
[JsonIgnore]
2018-05-11 15:30:26 +09:00
public HitWindows HitWindows { get ; set ; }
2018-04-13 18:19:50 +09:00
2018-11-05 12:15:45 +09:00
private readonly List < HitObject > nestedHitObjects = new List < HitObject > ( ) ;
2018-04-13 18:19:50 +09:00
2017-12-22 21:42:54 +09:00
[JsonIgnore]
2021-09-20 01:48:33 +09:00
public SlimReadOnlyListWrapper < HitObject > NestedHitObjects = > nestedHitObjects . AsSlimReadOnly ( ) ;
2018-04-13 18:19:50 +09:00
2017-03-16 16:55:08 +09:00
/// <summary>
/// Applies default values to this HitObject.
/// </summary>
2017-05-23 13:55:18 +09:00
/// <param name="controlPointInfo">The control points.</param>
2017-03-16 17:24:41 +09:00
/// <param name="difficulty">The difficulty settings to use.</param>
2020-05-15 18:07:41 +09:00
/// <param name="cancellationToken">The cancellation token.</param>
2021-10-01 14:56:42 +09:00
public void ApplyDefaults ( ControlPointInfo controlPointInfo , IBeatmapDifficultyInfo difficulty , CancellationToken cancellationToken = default )
2017-12-22 21:42:54 +09:00
{
2023-04-26 11:46:05 +02:00
ApplyDefaultsToSelf ( controlPointInfo , difficulty ) ;
2018-10-10 13:03:18 +09:00
nestedHitObjects . Clear ( ) ;
2018-05-17 12:29:33 +09:00
2020-05-15 18:07:41 +09:00
CreateNestedHitObjects ( cancellationToken ) ;
2018-05-17 12:29:33 +09:00
2019-09-26 17:39:19 +09:00
if ( this is IHasComboInformation hasCombo )
{
2021-09-20 01:48:33 +09:00
foreach ( HitObject hitObject in nestedHitObjects )
2019-09-26 17:39:19 +09:00
{
2021-09-20 01:48:33 +09:00
if ( hitObject is IHasComboInformation n )
{
n . ComboIndexBindable . BindTo ( hasCombo . ComboIndexBindable ) ;
n . ComboIndexWithOffsetsBindable . BindTo ( hasCombo . ComboIndexWithOffsetsBindable ) ;
n . IndexInCurrentComboBindable . BindTo ( hasCombo . IndexInCurrentComboBindable ) ;
}
2019-09-26 17:39:19 +09:00
}
}
2018-11-05 12:15:45 +09:00
nestedHitObjects . Sort ( ( h1 , h2 ) = > h1 . StartTime . CompareTo ( h2 . StartTime ) ) ;
2018-10-10 14:58:29 +09:00
foreach ( var h in nestedHitObjects )
2020-05-15 18:07:41 +09:00
h . ApplyDefaults ( controlPointInfo , difficulty , cancellationToken ) ;
2019-10-18 13:18:41 +09:00
2022-01-12 19:24:59 +01:00
// `ApplyDefaults()` may be called multiple times on a single hitobject.
// to prevent subscribing to `StartTimeBindable.ValueChanged` multiple times with the same callback,
// remove the previous subscription (if present) before (re-)registering.
StartTimeBindable . ValueChanged - = onStartTimeChanged ;
// this callback must be (re-)registered after default application
// to ensure that the read of `this.GetEndTime()` within `onStartTimeChanged` doesn't return an invalid value
2022-01-11 21:27:22 +01:00
// if `StartTimeBindable` is changed prior to default application.
2022-01-12 19:24:59 +01:00
StartTimeBindable . ValueChanged + = onStartTimeChanged ;
DefaultsApplied ? . Invoke ( this ) ;
void onStartTimeChanged ( ValueChangedEvent < double > time )
2022-01-11 21:27:22 +01:00
{
double offset = time . NewValue - time . OldValue ;
foreach ( var nested in nestedHitObjects )
nested . StartTime + = offset ;
2022-01-12 19:24:59 +01:00
}
2017-12-22 21:42:54 +09:00
}
2018-04-13 18:19:50 +09:00
2021-10-01 14:56:42 +09:00
protected virtual void ApplyDefaultsToSelf ( ControlPointInfo controlPointInfo , IBeatmapDifficultyInfo difficulty )
2017-04-05 21:59:07 +09:00
{
2018-12-04 12:01:30 +09:00
Kiai = controlPointInfo . EffectPointAt ( StartTime + control_point_leniency ) . KiaiMode ;
2018-04-13 18:19:50 +09:00
2020-06-03 16:48:44 +09:00
HitWindows ? ? = CreateHitWindows ( ) ;
2018-05-11 15:30:26 +09:00
HitWindows ? . SetDifficulty ( difficulty . OverallDifficulty ) ;
2017-04-05 21:59:07 +09:00
}
2018-04-13 18:19:50 +09:00
2020-05-15 18:07:41 +09:00
protected virtual void CreateNestedHitObjects ( CancellationToken cancellationToken )
2017-12-22 21:42:54 +09:00
{
}
2018-04-13 18:19:50 +09:00
2018-10-10 13:03:18 +09:00
protected void AddNested ( HitObject hitObject ) = > nestedHitObjects . Add ( hitObject ) ;
2018-05-11 15:30:26 +09:00
2018-08-06 10:55:38 +09:00
/// <summary>
/// Creates the <see cref="Judgement"/> that represents the scoring information for this <see cref="HitObject"/>.
/// </summary>
2020-02-23 13:01:30 +09:00
[NotNull]
2020-02-25 19:07:15 +09:00
public virtual Judgement CreateJudgement ( ) = > new Judgement ( ) ;
2018-08-06 10:55:38 +09:00
2018-05-11 15:30:26 +09:00
/// <summary>
/// Creates the <see cref="HitWindows"/> for this <see cref="HitObject"/>.
2019-09-02 17:15:36 +09:00
/// This can be null to indicate that the <see cref="HitObject"/> has no <see cref="HitWindows"/> and timing errors should not be displayed to the user.
2018-05-11 15:52:51 +09:00
/// <para>
2019-04-25 17:36:17 +09:00
/// This will only be invoked if <see cref="HitWindows"/> hasn't been set externally (e.g. from a <see cref="BeatmapConverter{T}"/>.
2018-05-11 15:52:51 +09:00
/// </para>
2018-05-11 15:30:26 +09:00
/// </summary>
2019-10-09 19:08:31 +09:00
[NotNull]
2020-02-25 19:07:15 +09:00
protected virtual HitWindows CreateHitWindows ( ) = > new HitWindows ( ) ;
2022-10-11 16:31:37 +09:00
2023-01-19 20:53:35 +09:00
/// <summary>
/// The maximum offset from the end time of <see cref="HitObject"/> at which this <see cref="HitObject"/> can be judged.
/// <para>
/// Defaults to the miss window.
/// </para>
/// </summary>
public virtual double MaximumJudgementOffset = > HitWindows ? . WindowFor ( HitResult . Miss ) ? ? 0 ;
2022-10-11 16:31:37 +09:00
public IList < HitSampleInfo > CreateSlidingSamples ( )
{
var slidingSamples = new List < HitSampleInfo > ( ) ;
var normalSample = Samples . FirstOrDefault ( s = > s . Name = = HitSampleInfo . HIT_NORMAL ) ;
if ( normalSample ! = null )
slidingSamples . Add ( normalSample . With ( "sliderslide" ) ) ;
var whistleSample = Samples . FirstOrDefault ( s = > s . Name = = HitSampleInfo . HIT_WHISTLE ) ;
if ( whistleSample ! = null )
slidingSamples . Add ( whistleSample . With ( "sliderwhistle" ) ) ;
return slidingSamples ;
}
2023-04-29 23:52:24 +02:00
/// <summary>
2023-05-17 14:07:48 +09:00
/// Create a <see cref="HitSampleInfo"/> based on the sample settings of the first <see cref="HitSampleInfo.HIT_NORMAL"/> sample in <see cref="Samples"/>.
/// If no sample is available, sane default settings will be used instead.
2023-04-29 23:52:24 +02:00
/// </summary>
2023-05-17 14:07:48 +09:00
/// <remarks>
/// In the case an existing sample exists, all settings apart from the sample name will be inherited. This includes volume, bank and suffix.
/// </remarks>
2023-04-29 23:52:24 +02:00
/// <param name="sampleName">The name of the sample.</param>
/// <returns>A populated <see cref="HitSampleInfo"/>.</returns>
2023-05-17 14:07:48 +09:00
public HitSampleInfo CreateHitSampleInfo ( string sampleName = HitSampleInfo . HIT_NORMAL )
2023-04-29 23:52:24 +02:00
{
2023-05-30 14:04:02 +09:00
// As per stable, all non-normal "addition" samples should use the same bank.
if ( sampleName ! = HitSampleInfo . HIT_NORMAL )
{
if ( Samples . FirstOrDefault ( s = > s . Name ! = HitSampleInfo . HIT_NORMAL ) is HitSampleInfo existingAddition )
return existingAddition . With ( newName : sampleName ) ;
}
// Fall back to using the normal sample bank otherwise.
if ( Samples . FirstOrDefault ( s = > s . Name = = HitSampleInfo . HIT_NORMAL ) is HitSampleInfo existingNormal )
return existingNormal . With ( newName : sampleName ) ;
2023-05-17 14:07:48 +09:00
return new HitSampleInfo ( sampleName ) ;
2023-04-29 23:52:24 +02:00
}
2016-08-31 12:33:01 +09:00
}
2019-11-25 19:01:24 +09:00
public static class HitObjectExtensions
{
/// <summary>
/// Returns the end time of this object.
/// </summary>
/// <remarks>
2020-05-27 12:38:39 +09:00
/// This returns the <see cref="IHasDuration.EndTime"/> where available, falling back to <see cref="HitObject.StartTime"/> otherwise.
2019-11-25 19:01:24 +09:00
/// </remarks>
/// <param name="hitObject">The object.</param>
/// <returns>The end time of this object.</returns>
2020-05-27 12:38:39 +09:00
public static double GetEndTime ( this HitObject hitObject ) = > ( hitObject as IHasDuration ) ? . EndTime ? ? hitObject . StartTime ;
2019-11-25 19:01:24 +09:00
}
2016-08-31 12:33:01 +09:00
}