2019-01-24 16:43:03 +08: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 17:19:50 +08:00
using System ;
using System.Collections.Generic ;
2020-11-06 21:15:00 +08:00
using System.Collections.Specialized ;
2018-04-13 17:19:50 +08:00
using System.Linq ;
2019-10-21 16:56:39 +08:00
using JetBrains.Annotations ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Allocation ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2018-08-06 10:07:05 +08:00
using osu.Framework.Extensions.TypeExtensions ;
2019-08-28 17:12:47 +08:00
using osu.Framework.Graphics ;
2018-09-12 14:09:10 +08:00
using osu.Framework.Graphics.Primitives ;
2020-10-01 11:28:33 +08:00
using osu.Framework.Logging ;
2019-09-02 14:02:16 +08:00
using osu.Framework.Threading ;
2018-04-13 17:19:50 +08:00
using osu.Game.Audio ;
using osu.Game.Rulesets.Judgements ;
using osu.Game.Rulesets.Objects.Types ;
using osu.Game.Rulesets.Scoring ;
using osu.Game.Skinning ;
2020-04-10 00:12:15 +08:00
using osu.Game.Configuration ;
2020-11-12 14:24:45 +08:00
using osu.Game.Rulesets.UI ;
2018-11-20 15:51:59 +08:00
using osuTK.Graphics ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.Objects.Drawables
{
2019-07-19 18:12:41 +08:00
[Cached(typeof(DrawableHitObject))]
2019-07-22 13:45:25 +08:00
public abstract class DrawableHitObject : SkinReloadableDrawable
2018-04-13 17:19:50 +08:00
{
2020-11-11 15:35:48 +08:00
/// <summary>
/// Invoked after this <see cref="DrawableHitObject"/>'s applied <see cref="HitObject"/> has had its defaults applied.
/// </summary>
2020-05-08 17:49:58 +08:00
public event Action < DrawableHitObject > DefaultsApplied ;
2020-11-11 15:35:48 +08:00
/// <summary>
/// Invoked after a <see cref="HitObject"/> has been applied to this <see cref="DrawableHitObject"/>.
/// </summary>
public event Action < DrawableHitObject > HitObjectApplied ;
2020-11-06 21:15:00 +08:00
/// <summary>
/// The <see cref="HitObject"/> currently represented by this <see cref="DrawableHitObject"/>.
/// </summary>
public HitObject HitObject { get ; private set ; }
2018-04-13 17:19:50 +08:00
2020-12-03 19:03:39 +08:00
/// <summary>
/// The parenting <see cref="DrawableHitObject"/>, if any.
/// </summary>
[CanBeNull]
protected internal DrawableHitObject ParentHitObject { get ; internal set ; }
2018-04-13 17:19:50 +08:00
/// <summary>
/// The colour used for various elements of this DrawableHitObject.
/// </summary>
2019-07-22 13:45:25 +08:00
public readonly Bindable < Color4 > AccentColour = new Bindable < Color4 > ( Color4 . Gray ) ;
2018-04-13 17:19:50 +08:00
2020-09-30 14:45:14 +08:00
protected PausableSkinnableSound Samples { get ; private set ; }
2018-04-13 17:19:50 +08:00
2020-05-19 22:28:13 +08:00
public virtual IEnumerable < HitSampleInfo > GetSamples ( ) = > HitObject . Samples ;
2018-04-13 17:19:50 +08:00
private readonly Lazy < List < DrawableHitObject > > nestedHitObjects = new Lazy < List < DrawableHitObject > > ( ) ;
2019-10-16 21:10:50 +08:00
public IReadOnlyList < DrawableHitObject > NestedHitObjects = > nestedHitObjects . IsValueCreated ? nestedHitObjects . Value : ( IReadOnlyList < DrawableHitObject > ) Array . Empty < DrawableHitObject > ( ) ;
2018-04-13 17:19:50 +08:00
2020-03-29 13:30:45 +08:00
/// <summary>
/// Whether this object should handle any user input events.
/// </summary>
public bool HandleUserInput { get ; set ; } = true ;
public override bool PropagatePositionalInputSubTree = > HandleUserInput ;
public override bool PropagateNonPositionalInputSubTree = > HandleUserInput ;
2018-08-06 09:54:16 +08:00
/// <summary>
2020-10-01 20:54:43 +08:00
/// Invoked by this or a nested <see cref="DrawableHitObject"/> after a <see cref="JudgementResult"/> has been applied.
2018-08-06 09:54:16 +08:00
/// </summary>
public event Action < DrawableHitObject , JudgementResult > OnNewResult ;
/// <summary>
2020-10-01 20:54:43 +08:00
/// Invoked by this or a nested <see cref="DrawableHitObject"/> prior to a <see cref="JudgementResult"/> being reverted.
2018-08-06 09:54:16 +08:00
/// </summary>
2018-08-06 11:29:22 +08:00
public event Action < DrawableHitObject , JudgementResult > OnRevertResult ;
2018-04-13 17:19:50 +08:00
2020-11-20 23:27:19 +08:00
/// <summary>
/// Invoked when a new nested hit object is created by <see cref="CreateNestedHitObject" />.
/// </summary>
internal event Action < DrawableHitObject > OnNestedDrawableCreated ;
2018-04-13 17:19:50 +08:00
/// <summary>
2018-08-06 10:31:54 +08:00
/// Whether a visual indicator should be displayed when a scoring result occurs.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-08-06 10:31:46 +08:00
public virtual bool DisplayResult = > true ;
2018-04-13 17:19:50 +08:00
/// <summary>
2018-08-03 15:07:20 +08:00
/// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been judged.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-08-03 15:07:20 +08:00
public bool AllJudged = > Judged & & NestedHitObjects . All ( h = > h . AllJudged ) ;
2018-04-13 17:19:50 +08:00
/// <summary>
2019-04-25 16:36:17 +08:00
/// Whether this <see cref="DrawableHitObject"/> has been hit. This occurs if <see cref="Result"/> is hit.
2018-08-03 15:07:20 +08:00
/// Note: This does NOT include nested hitobjects.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-08-03 15:07:20 +08:00
public bool IsHit = > Result ? . IsHit ? ? false ;
2018-04-13 17:19:50 +08:00
/// <summary>
2018-08-01 20:04:03 +08:00
/// Whether this <see cref="DrawableHitObject"/> has been judged.
/// Note: This does NOT include nested hitobjects.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-08-03 14:38:48 +08:00
public bool Judged = > Result ? . HasResult ? ? true ;
2018-08-02 19:35:54 +08:00
2018-08-06 09:55:38 +08:00
/// <summary>
/// The scoring result of this <see cref="DrawableHitObject"/>.
/// </summary>
2018-08-06 10:07:05 +08:00
public JudgementResult Result { get ; private set ; }
2018-08-02 20:07:31 +08:00
2020-04-10 00:12:15 +08:00
/// <summary>
2020-04-13 12:00:03 +08:00
/// The relative X position of this hit object for sample playback balance adjustment.
2020-04-10 00:12:15 +08:00
/// </summary>
2020-04-13 12:00:03 +08:00
/// <remarks>
/// This is a range of 0..1 (0 for far-left, 0.5 for centre, 1 for far-right).
/// Dampening is post-applied to ensure the effect is not too intense.
/// </remarks>
protected virtual float SamplePlaybackPosition = > 0.5f ;
2020-04-12 07:33:25 +08:00
2020-11-10 21:49:02 +08:00
public readonly Bindable < double > StartTimeBindable = new Bindable < double > ( ) ;
2020-11-06 21:15:00 +08:00
private readonly BindableList < HitSampleInfo > samplesBindable = new BindableList < HitSampleInfo > ( ) ;
private readonly Bindable < bool > userPositionalHitSounds = new Bindable < bool > ( ) ;
private readonly Bindable < int > comboIndexBindable = new Bindable < int > ( ) ;
2019-09-26 16:04:38 +08:00
2018-04-13 17:19:50 +08:00
public override bool RemoveWhenNotAlive = > false ;
public override bool RemoveCompletedTransforms = > false ;
protected override bool RequiresChildrenUpdate = > true ;
2019-07-23 20:08:41 +08:00
public override bool IsPresent = > base . IsPresent | | ( State . Value = = ArmedState . Idle & & Clock ? . CurrentTime > = LifetimeStart ) ;
2018-04-13 17:19:50 +08:00
2019-07-22 14:05:56 +08:00
private readonly Bindable < ArmedState > state = new Bindable < ArmedState > ( ) ;
2020-11-26 22:42:05 +08:00
/// <summary>
/// The state of this <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// For pooled hitobjects, <see cref="ApplyCustomUpdateState"/> is recommended to be used instead for better editor/rewinding support.
/// </remarks>
2019-07-23 20:08:41 +08:00
public IBindable < ArmedState > State = > state ;
2020-11-06 23:25:26 +08:00
/// <summary>
/// Whether <see cref="HitObject"/> is currently applied.
/// </summary>
private bool hasHitObjectApplied ;
2020-11-10 19:16:52 +08:00
/// <summary>
/// The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of the currently-attached <see cref="HitObject"/>.
/// </summary>
[CanBeNull]
private HitObjectLifetimeEntry lifetimeEntry ;
2020-11-12 14:24:45 +08:00
[Resolved(CanBeNull = true)]
2020-11-13 23:54:57 +08:00
private IPooledHitObjectProvider pooledObjectProvider { get ; set ; }
2020-11-12 14:24:45 +08:00
2020-11-21 10:19:52 +08:00
/// <summary>
/// Whether the initialization logic in <see cref="Playfield" /> has applied.
/// </summary>
2020-11-22 17:47:35 +08:00
internal bool IsInitialized ;
2020-11-21 10:19:52 +08:00
2020-11-06 21:15:00 +08:00
/// <summary>
/// Creates a new <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="initialHitObject">
/// The <see cref="HitObject"/> to be initially applied to this <see cref="DrawableHitObject"/>.
/// If <c>null</c>, a hitobject is expected to be later applied via <see cref="Apply"/> (or automatically via pooling).
/// </param>
protected DrawableHitObject ( [ CanBeNull ] HitObject initialHitObject = null )
2018-04-13 17:19:50 +08:00
{
2020-11-06 21:15:00 +08:00
HitObject = initialHitObject ;
2018-04-13 17:19:50 +08:00
}
[BackgroundDependencyLoader]
2020-04-10 00:12:15 +08:00
private void load ( OsuConfigManager config )
2018-04-13 17:19:50 +08:00
{
2020-11-06 21:15:00 +08:00
config . BindWith ( OsuSetting . PositionalHitSounds , userPositionalHitSounds ) ;
2020-11-12 17:48:25 +08:00
// Explicit non-virtual function call.
2020-11-19 19:38:36 +08:00
base . AddInternal ( Samples = new PausableSkinnableSound ( ) ) ;
2018-04-13 17:19:50 +08:00
}
2020-07-24 17:16:36 +08:00
protected override void LoadAsyncComplete ( )
2018-04-13 17:19:50 +08:00
{
2020-07-24 17:16:36 +08:00
base . LoadAsyncComplete ( ) ;
2019-09-26 16:04:38 +08:00
2020-11-06 21:15:00 +08:00
if ( HitObject ! = null )
2020-11-10 19:16:52 +08:00
Apply ( HitObject , lifetimeEntry ) ;
2020-11-06 21:15:00 +08:00
}
2020-11-05 12:51:46 +08:00
2020-11-06 21:15:00 +08:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2019-10-18 12:18:41 +08:00
2020-11-27 10:41:39 +08:00
comboIndexBindable . BindValueChanged ( _ = > UpdateComboColour ( ) , true ) ;
2019-10-16 21:10:50 +08:00
2020-11-06 21:15:00 +08:00
updateState ( ArmedState . Idle , true ) ;
}
/// <summary>
/// Applies a new <see cref="HitObject"/> to be represented by this <see cref="DrawableHitObject"/>.
/// </summary>
2020-11-06 23:25:26 +08:00
/// <param name="hitObject">The <see cref="HitObject"/> to apply.</param>
2020-11-10 19:16:52 +08:00
/// <param name="lifetimeEntry">The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of <paramref name="hitObject"/>.</param>
public void Apply ( [ NotNull ] HitObject hitObject , [ CanBeNull ] HitObjectLifetimeEntry lifetimeEntry )
2020-07-24 17:16:36 +08:00
{
2020-11-06 23:40:26 +08:00
free ( ) ;
2020-11-06 22:03:29 +08:00
2020-11-06 23:25:26 +08:00
HitObject = hitObject ? ? throw new InvalidOperationException ( $"Cannot apply a null {nameof(HitObject)}." ) ;
2020-07-15 15:53:27 +08:00
2020-11-10 19:16:52 +08:00
this . lifetimeEntry = lifetimeEntry ;
if ( lifetimeEntry ! = null )
{
// Transfer lifetime from the entry.
LifetimeStart = lifetimeEntry . LifetimeStart ;
LifetimeEnd = lifetimeEntry . LifetimeEnd ;
// Copy any existing result from the entry (required for rewind / judgement revert).
Result = lifetimeEntry . Result ;
}
2020-11-16 20:47:33 +08:00
else
LifetimeStart = HitObject . StartTime - InitialLifetimeOffset ;
2020-11-10 19:16:52 +08:00
2020-11-06 21:15:00 +08:00
// Ensure this DHO has a result.
Result ? ? = CreateResult ( HitObject . CreateJudgement ( ) )
? ? throw new InvalidOperationException ( $"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}." ) ;
2020-11-10 19:16:52 +08:00
// Copy back the result to the entry for potential future retrieval.
if ( lifetimeEntry ! = null )
lifetimeEntry . Result = Result ;
2020-11-06 21:15:00 +08:00
foreach ( var h in HitObject . NestedHitObjects )
{
2020-12-03 18:46:42 +08:00
var pooledDrawableNested = pooledObjectProvider ? . GetPooledDrawableRepresentation ( h , this ) ;
2020-11-20 23:27:19 +08:00
var drawableNested = pooledDrawableNested
2020-11-12 14:24:45 +08:00
? ? CreateNestedHitObject ( h )
? ? throw new InvalidOperationException ( $"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}." ) ;
2020-11-06 21:15:00 +08:00
2020-12-03 18:46:42 +08:00
// Only invoke the event for non-pooled DHOs, otherwise the event will be fired by the playfield.
2020-11-20 23:27:19 +08:00
if ( pooledDrawableNested = = null )
OnNestedDrawableCreated ? . Invoke ( drawableNested ) ;
2020-11-09 18:06:48 +08:00
drawableNested . OnNewResult + = onNewResult ;
drawableNested . OnRevertResult + = onRevertResult ;
drawableNested . ApplyCustomUpdateState + = onApplyCustomUpdateState ;
2020-11-06 21:15:00 +08:00
2020-12-03 19:03:39 +08:00
// This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation().
// Must be done before the nested DHO is added to occur before the nested Apply()!
drawableNested . ParentHitObject = this ;
2020-12-03 18:46:42 +08:00
2020-11-06 21:15:00 +08:00
nestedHitObjects . Value . Add ( drawableNested ) ;
AddNestedHitObject ( drawableNested ) ;
}
2020-11-10 21:49:02 +08:00
StartTimeBindable . BindTo ( HitObject . StartTimeBindable ) ;
2020-11-13 13:33:23 +08:00
StartTimeBindable . BindValueChanged ( onStartTimeChanged ) ;
2020-11-06 21:15:00 +08:00
if ( HitObject is IHasComboInformation combo )
comboIndexBindable . BindTo ( combo . ComboIndexBindable ) ;
samplesBindable . BindTo ( HitObject . SamplesBindable ) ;
samplesBindable . BindCollectionChanged ( onSamplesChanged , true ) ;
HitObject . DefaultsApplied + = onDefaultsApplied ;
2020-11-27 09:13:05 +08:00
OnApply ( ) ;
2020-11-11 15:35:48 +08:00
HitObjectApplied ? . Invoke ( this ) ;
2020-11-09 23:30:23 +08:00
2020-11-27 15:31:59 +08:00
// If not loaded, the state update happens in LoadComplete().
2020-11-06 21:15:00 +08:00
if ( IsLoaded )
2020-11-25 16:54:03 +08:00
{
2020-11-27 15:31:59 +08:00
if ( Result . IsHit )
updateState ( ArmedState . Hit , true ) ;
else if ( Result . HasResult )
updateState ( ArmedState . Miss , true ) ;
else
updateState ( ArmedState . Idle , true ) ;
2020-11-25 16:54:03 +08:00
}
2020-11-06 23:25:26 +08:00
hasHitObjectApplied = true ;
}
/// <summary>
/// Removes the currently applied <see cref="HitObject"/>
/// </summary>
2020-11-06 23:40:26 +08:00
private void free ( )
2020-11-06 23:25:26 +08:00
{
2020-11-06 23:40:26 +08:00
if ( ! hasHitObjectApplied )
return ;
2020-11-10 21:49:02 +08:00
StartTimeBindable . UnbindFrom ( HitObject . StartTimeBindable ) ;
2020-11-06 23:25:26 +08:00
if ( HitObject is IHasComboInformation combo )
comboIndexBindable . UnbindFrom ( combo . ComboIndexBindable ) ;
samplesBindable . UnbindFrom ( HitObject . SamplesBindable ) ;
2020-11-13 13:33:23 +08:00
// Changes in start time trigger state updates. When a new hitobject is applied, OnApply() automatically performs a state update anyway.
StartTimeBindable . ValueChanged - = onStartTimeChanged ;
2020-11-06 23:25:26 +08:00
// When a new hitobject is applied, the samples will be cleared before re-populating.
// In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply().
samplesBindable . CollectionChanged - = onSamplesChanged ;
2020-11-19 19:38:36 +08:00
// Release the samples for other hitobjects to use.
Samples . Samples = null ;
2020-11-19 18:51:09 +08:00
2020-11-06 23:25:26 +08:00
if ( nestedHitObjects . IsValueCreated )
{
foreach ( var obj in nestedHitObjects . Value )
{
obj . OnNewResult - = onNewResult ;
obj . OnRevertResult - = onRevertResult ;
obj . ApplyCustomUpdateState - = onApplyCustomUpdateState ;
}
nestedHitObjects . Value . Clear ( ) ;
ClearNestedHitObjects ( ) ;
}
HitObject . DefaultsApplied - = onDefaultsApplied ;
2020-11-27 09:13:05 +08:00
OnFree ( ) ;
2020-11-06 23:40:26 +08:00
HitObject = null ;
2020-12-03 19:03:39 +08:00
ParentHitObject = null ;
2020-11-15 04:09:22 +08:00
Result = null ;
2020-11-10 23:24:14 +08:00
lifetimeEntry = null ;
2020-11-16 02:45:49 +08:00
clearExistingStateTransforms ( ) ;
2020-11-06 23:25:26 +08:00
hasHitObjectApplied = false ;
}
protected sealed override void FreeAfterUse ( )
{
base . FreeAfterUse ( ) ;
2020-11-06 23:40:26 +08:00
// Freeing while not in a pool would cause the DHO to not be usable elsewhere in the hierarchy without being re-applied.
2020-11-06 23:25:26 +08:00
if ( ! IsInPool )
return ;
2020-11-06 23:40:26 +08:00
free ( ) ;
}
/// <summary>
/// Invoked for this <see cref="DrawableHitObject"/> to take on any values from a newly-applied <see cref="HitObject"/>.
/// </summary>
2020-11-27 09:13:05 +08:00
protected virtual void OnApply ( )
2020-11-06 23:40:26 +08:00
{
}
/// <summary>
/// Invoked for this <see cref="DrawableHitObject"/> to revert any values previously taken on from the currently-applied <see cref="HitObject"/>.
/// </summary>
2020-11-27 09:13:05 +08:00
protected virtual void OnFree ( )
2020-11-06 23:40:26 +08:00
{
2019-07-22 14:05:56 +08:00
}
2020-09-23 17:09:40 +08:00
/// <summary>
2020-09-23 17:12:07 +08:00
/// Invoked by the base <see cref="DrawableHitObject"/> to populate samples, once on initial load and potentially again on any change to the samples collection.
2020-09-23 17:09:40 +08:00
/// </summary>
2020-09-24 12:28:29 +08:00
protected virtual void LoadSamples ( )
2019-11-08 13:59:47 +08:00
{
var samples = GetSamples ( ) . ToArray ( ) ;
if ( samples . Length < = 0 )
return ;
if ( HitObject . SampleControlPoint = = null )
2019-11-12 12:41:54 +08:00
{
2019-11-28 22:21:21 +08:00
throw new InvalidOperationException ( $"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}." ) ;
2019-11-12 12:41:54 +08:00
}
2019-11-08 13:59:47 +08:00
2020-11-19 18:51:09 +08:00
Samples . Samples = samples . Select ( s = > HitObject . SampleControlPoint . ApplyTo ( s ) ) . Cast < ISampleInfo > ( ) . ToArray ( ) ;
2019-11-08 13:59:47 +08:00
}
2020-11-06 21:15:00 +08:00
private void onSamplesChanged ( object sender , NotifyCollectionChangedEventArgs e ) = > LoadSamples ( ) ;
2019-10-18 12:18:41 +08:00
2020-11-13 13:33:23 +08:00
private void onStartTimeChanged ( ValueChangedEvent < double > startTime ) = > updateState ( State . Value , true ) ;
2020-11-06 21:15:00 +08:00
private void onNewResult ( DrawableHitObject drawableHitObject , JudgementResult result ) = > OnNewResult ? . Invoke ( drawableHitObject , result ) ;
2019-10-16 21:10:50 +08:00
2020-11-06 21:15:00 +08:00
private void onRevertResult ( DrawableHitObject drawableHitObject , JudgementResult result ) = > OnRevertResult ? . Invoke ( drawableHitObject , result ) ;
2020-04-22 17:32:59 +08:00
2020-11-06 21:15:00 +08:00
private void onApplyCustomUpdateState ( DrawableHitObject drawableHitObject , ArmedState state ) = > ApplyCustomUpdateState ? . Invoke ( drawableHitObject , state ) ;
2020-04-22 17:32:59 +08:00
2020-11-06 21:15:00 +08:00
private void onDefaultsApplied ( HitObject hitObject )
{
2020-11-10 19:16:52 +08:00
Apply ( hitObject , lifetimeEntry ) ;
2020-11-06 21:15:00 +08:00
DefaultsApplied ? . Invoke ( this ) ;
2019-10-16 21:10:50 +08:00
}
2019-10-17 12:52:21 +08:00
/// <summary>
/// Invoked by the base <see cref="DrawableHitObject"/> to add nested <see cref="DrawableHitObject"/>s to the hierarchy.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to be added.</param>
protected virtual void AddNestedHitObject ( DrawableHitObject hitObject )
2019-10-16 21:10:50 +08:00
{
}
2019-10-17 12:52:21 +08:00
/// <summary>
/// Invoked by the base <see cref="DrawableHitObject"/> to remove all previously-added nested <see cref="DrawableHitObject"/>s.
/// </summary>
protected virtual void ClearNestedHitObjects ( )
{
}
/// <summary>
/// Creates the drawable representation for a nested <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/>.</param>
/// <returns>The drawable representation for <paramref name="hitObject"/>.</returns>
protected virtual DrawableHitObject CreateNestedHitObject ( HitObject hitObject ) = > null ;
2019-10-16 21:10:50 +08:00
2019-07-22 14:33:12 +08:00
#region State / Transform Management
2019-07-22 15:08:38 +08:00
/// <summary>
2020-10-01 20:54:43 +08:00
/// Invoked by this or a nested <see cref="DrawableHitObject"/> to apply a custom state that can override the default implementation.
2019-07-22 15:08:38 +08:00
/// </summary>
public event Action < DrawableHitObject , ArmedState > ApplyCustomUpdateState ;
2019-07-22 14:33:12 +08:00
protected override void ClearInternal ( bool disposeChildren = true ) = > throw new InvalidOperationException ( $"Should never clear a {nameof(DrawableHitObject)}" ) ;
2019-07-23 20:08:41 +08:00
private void updateState ( ArmedState newState , bool force = false )
2019-07-22 14:05:56 +08:00
{
2019-07-23 20:08:41 +08:00
if ( State . Value = = newState & & ! force )
return ;
2020-02-27 10:28:29 +08:00
LifetimeEnd = double . MaxValue ;
2019-09-07 13:44:44 +08:00
2020-02-27 10:28:29 +08:00
double transformTime = HitObject . StartTime - InitialLifetimeOffset ;
2019-07-22 14:05:56 +08:00
2020-11-16 02:45:49 +08:00
clearExistingStateTransforms ( ) ;
2019-07-22 14:05:56 +08:00
2020-02-27 10:28:29 +08:00
using ( BeginAbsoluteSequence ( transformTime , true ) )
UpdateInitialTransforms ( ) ;
2018-04-13 17:19:50 +08:00
2020-11-04 15:49:34 +08:00
using ( BeginAbsoluteSequence ( StateUpdateTime , true ) )
UpdateStartTimeStateTransforms ( ) ;
2020-11-04 15:27:47 +08:00
#pragma warning disable 618
using ( BeginAbsoluteSequence ( StateUpdateTime + ( Result ? . TimeOffset ? ? 0 ) , true ) )
2020-11-04 15:04:15 +08:00
UpdateStateTransforms ( newState ) ;
2020-11-04 15:27:47 +08:00
#pragma warning restore 618
2020-11-04 15:39:39 +08:00
using ( BeginAbsoluteSequence ( HitStateUpdateTime , true ) )
UpdateHitStateTransforms ( newState ) ;
2019-07-22 14:05:56 +08:00
2020-11-04 15:04:15 +08:00
state . Value = newState ;
2020-05-22 11:45:37 +08:00
if ( LifetimeEnd = = double . MaxValue & & ( state . Value ! = ArmedState . Idle | | HitObject . HitWindows = = null ) )
2020-12-03 11:32:49 +08:00
LifetimeEnd = Math . Max ( LatestTransformEndTime , HitStateUpdateTime + ( Samples ? . Length ? ? 0 ) ) ;
2019-07-27 05:22:40 +08:00
// apply any custom state overrides
ApplyCustomUpdateState ? . Invoke ( this , newState ) ;
2020-10-07 17:18:01 +08:00
if ( ! force & & newState = = ArmedState . Hit )
2019-07-27 05:22:40 +08:00
PlaySamples ( ) ;
2018-04-13 17:19:50 +08:00
}
2020-11-16 02:45:49 +08:00
private void clearExistingStateTransforms ( )
{
base . ApplyTransformsAt ( double . MinValue , true ) ;
2020-11-23 14:18:54 +08:00
// has to call this method directly (not ClearTransforms) to bypass the local ClearTransformsAfter override.
2020-11-16 02:45:49 +08:00
base . ClearTransformsAfter ( double . MinValue , true ) ;
}
2019-07-22 15:08:38 +08:00
/// <summary>
2019-07-23 20:15:55 +08:00
/// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time.
2019-07-22 15:08:38 +08:00
/// The local drawable hierarchy is recursively delayed to <see cref="LifetimeStart"/> for convenience.
2019-08-28 17:12:47 +08:00
///
/// By default this will fade in the object from zero with no duration.
2019-07-22 15:08:38 +08:00
/// </summary>
/// <remarks>
/// This is called once before every <see cref="UpdateStateTransforms"/>. This is to ensure a good state in the case
/// the <see cref="JudgementResult.TimeOffset"/> was negative and potentially altered the pre-hit transforms.
/// </remarks>
2019-07-22 14:33:12 +08:00
protected virtual void UpdateInitialTransforms ( )
2019-07-22 14:05:56 +08:00
{
2019-08-28 17:12:47 +08:00
this . FadeInFromZero ( ) ;
2019-07-22 14:05:56 +08:00
}
2019-07-22 15:08:38 +08:00
/// <summary>
/// Apply transforms based on the current <see cref="ArmedState"/>. Previous states are automatically cleared.
2019-09-12 18:29:08 +08:00
/// In the case of a non-idle <see cref="ArmedState"/>, and if <see cref="Drawable.LifetimeEnd"/> was not set during this call, <see cref="Drawable.Expire"/> will be invoked.
2019-07-22 15:08:38 +08:00
/// </summary>
/// <param name="state">The new armed state.</param>
2020-11-04 15:27:47 +08:00
[Obsolete("Use UpdateStartTimeStateTransforms and UpdateHitStateTransforms instead")] // Can be removed 20210504
2019-07-22 14:33:12 +08:00
protected virtual void UpdateStateTransforms ( ArmedState state )
2019-07-22 14:05:56 +08:00
{
}
2020-11-04 15:27:47 +08:00
/// <summary>
/// Apply passive transforms at the <see cref="HitObject"/>'s StartTime.
/// This is called each time <see cref="State"/> changes.
/// Previous states are automatically cleared.
/// </summary>
protected virtual void UpdateStartTimeStateTransforms ( )
{
}
2020-11-04 15:04:15 +08:00
/// <summary>
/// Apply transforms based on the current <see cref="ArmedState"/>. This call is offset by <see cref="HitStateUpdateTime"/> (HitObject.EndTime + Result.Offset), equivalent to when the user hit the object.
2020-11-04 15:39:39 +08:00
/// If <see cref="Drawable.LifetimeEnd"/> was not set during this call, <see cref="Drawable.Expire"/> will be invoked.
2020-11-04 15:04:15 +08:00
/// Previous states are automatically cleared.
/// </summary>
/// <param name="state">The new armed state.</param>
protected virtual void UpdateHitStateTransforms ( ArmedState state )
{
}
2019-07-22 14:05:56 +08:00
public override void ClearTransformsAfter ( double time , bool propagateChildren = false , string targetMember = null )
{
2020-02-27 10:28:29 +08:00
// Parent calls to this should be blocked for safety, as we are manually handling this in updateState.
2019-07-22 14:05:56 +08:00
}
public override void ApplyTransformsAt ( double time , bool propagateChildren = false )
{
2020-02-27 10:28:29 +08:00
// Parent calls to this should be blocked for safety, as we are manually handling this in updateState.
2019-07-22 14:05:56 +08:00
}
2019-07-22 14:33:12 +08:00
#endregion
2019-09-18 19:19:57 +08:00
protected sealed override void SkinChanged ( ISkinSource skin , bool allowFallback )
2019-07-22 14:33:12 +08:00
{
base . SkinChanged ( skin , allowFallback ) ;
2020-11-27 10:41:39 +08:00
UpdateComboColour ( ) ;
2019-09-18 19:19:57 +08:00
ApplySkin ( skin , allowFallback ) ;
2019-09-19 13:15:06 +08:00
if ( IsLoaded )
updateState ( State . Value , true ) ;
2019-09-18 19:19:57 +08:00
}
2020-11-27 10:41:39 +08:00
protected void UpdateComboColour ( )
2019-09-26 16:04:38 +08:00
{
2020-11-26 17:14:25 +08:00
if ( ! ( HitObject is IHasComboInformation combo ) ) return ;
2020-02-20 14:14:40 +08:00
2020-11-26 17:14:25 +08:00
var comboColours = CurrentSkin . GetConfig < GlobalSkinColours , IReadOnlyList < Color4 > > ( GlobalSkinColours . ComboColours ) ? . Value ? ? Array . Empty < Color4 > ( ) ;
AccentColour . Value = combo . GetComboColour ( comboColours ) ;
2019-09-26 16:04:38 +08:00
}
2020-02-19 14:05:04 +08:00
/// <summary>
2020-02-20 14:14:40 +08:00
/// Called to retrieve the combo colour. Automatically assigned to <see cref="AccentColour"/>.
/// Defaults to using <see cref="IHasComboInformation.ComboIndex"/> to decide on a colour.
2020-02-19 14:05:04 +08:00
/// </summary>
2020-02-20 14:14:40 +08:00
/// <remarks>
/// This will only be called if the <see cref="HitObject"/> implements <see cref="IHasComboInformation"/>.
/// </remarks>
2020-02-19 14:05:04 +08:00
/// <param name="comboColours">A list of combo colours provided by the beatmap or skin. Can be null if not available.</param>
2020-11-26 17:14:25 +08:00
[Obsolete("Unused. Implement IHasComboInformation and IHasComboInformation.GetComboColour() on the HitObject model instead.")] // Can be removed 20210527
2020-02-20 14:14:40 +08:00
protected virtual Color4 GetComboColour ( IReadOnlyList < Color4 > comboColours )
2020-02-19 14:05:04 +08:00
{
2020-02-20 14:14:40 +08:00
if ( ! ( HitObject is IHasComboInformation combo ) )
throw new InvalidOperationException ( $"{nameof(HitObject)} must implement {nameof(IHasComboInformation)}" ) ;
return comboColours ? . Count > 0 ? comboColours [ combo . ComboIndex % comboColours . Count ] : Color4 . White ;
2020-02-19 14:05:04 +08:00
}
2019-09-18 19:19:57 +08:00
/// <summary>
/// Called when a change is made to the skin.
/// </summary>
/// <param name="skin">The new skin.</param>
/// <param name="allowFallback">Whether fallback to default skin should be allowed if the custom skin is missing this resource.</param>
protected virtual void ApplySkin ( ISkinSource skin , bool allowFallback )
{
2019-07-22 14:33:12 +08:00
}
2020-07-22 15:37:24 +08:00
/// <summary>
/// Calculate the position to be used for sample playback at a specified X position (0..1).
/// </summary>
/// <param name="position">The lookup X position. Generally should be <see cref="SamplePlaybackPosition"/>.</param>
/// <returns></returns>
protected double CalculateSamplePlaybackBalance ( double position )
{
const float balance_adjust_amount = 0.4f ;
return balance_adjust_amount * ( userPositionalHitSounds . Value ? position - 0.5f : 0 ) ;
}
2018-04-13 17:19:50 +08:00
/// <summary>
2018-09-15 22:30:11 +08:00
/// Plays all the hit sounds for this <see cref="DrawableHitObject"/>.
2018-12-03 16:21:27 +08:00
/// This is invoked automatically when this <see cref="DrawableHitObject"/> is hit.
2018-04-13 17:19:50 +08:00
/// </summary>
2020-04-10 06:01:35 +08:00
public virtual void PlaySamples ( )
{
2020-09-29 11:45:20 +08:00
if ( Samples ! = null )
2020-05-20 19:49:01 +08:00
{
2020-07-22 15:37:24 +08:00
Samples . Balance . Value = CalculateSamplePlaybackBalance ( SamplePlaybackPosition ) ;
2020-05-20 19:49:01 +08:00
Samples . Play ( ) ;
}
2020-04-10 06:01:35 +08:00
}
2018-04-13 17:19:50 +08:00
2020-09-29 14:07:55 +08:00
/// <summary>
2020-10-05 15:24:02 +08:00
/// Stops playback of all relevant samples. Generally only looping samples should be stopped by this, and the rest let to play out.
/// Automatically called when <see cref="DrawableHitObject{TObject}"/>'s lifetime has been exceeded.
2020-09-29 14:07:55 +08:00
/// </summary>
2020-10-05 15:24:02 +08:00
public virtual void StopAllSamples ( )
2020-10-05 14:07:46 +08:00
{
if ( Samples ? . Looping = = true )
Samples . Stop ( ) ;
2020-10-05 14:12:34 +08:00
}
2020-09-29 14:07:55 +08:00
2018-04-13 17:19:50 +08:00
protected override void Update ( )
{
base . Update ( ) ;
2018-08-14 17:31:32 +08:00
if ( Result ! = null & & Result . HasResult )
2018-04-13 17:19:50 +08:00
{
2019-11-25 18:01:24 +08:00
var endTime = HitObject . GetEndTime ( ) ;
2018-04-13 17:19:50 +08:00
2018-08-14 17:31:32 +08:00
if ( Result . TimeOffset + endTime > Time . Current )
2018-08-01 20:04:03 +08:00
{
2018-08-06 11:29:22 +08:00
OnRevertResult ? . Invoke ( this , Result ) ;
2018-08-01 20:04:03 +08:00
2019-06-13 13:55:52 +08:00
Result . TimeOffset = 0 ;
2018-08-03 14:38:48 +08:00
Result . Type = HitResult . None ;
2019-07-23 20:08:41 +08:00
updateState ( ArmedState . Idle ) ;
2018-08-01 20:04:03 +08:00
}
2018-04-13 17:19:50 +08:00
}
}
2020-04-20 19:48:35 +08:00
public override bool UpdateSubTreeMasking ( Drawable source , RectangleF maskingBounds ) = > false ;
2018-09-12 14:09:10 +08:00
2018-04-13 17:19:50 +08:00
protected override void UpdateAfterChildren ( )
{
base . UpdateAfterChildren ( ) ;
2018-08-06 10:31:46 +08:00
UpdateResult ( false ) ;
2018-04-13 17:19:50 +08:00
}
2019-09-02 14:02:16 +08:00
/// <summary>
/// Schedules an <see cref="Action"/> to this <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// Only provided temporarily until hitobject pooling is implemented.
/// </remarks>
protected internal new ScheduledDelegate Schedule ( Action action ) = > base . Schedule ( action ) ;
2019-07-16 12:45:59 +08:00
public override double LifetimeStart
{
2020-11-10 19:16:52 +08:00
get = > base . LifetimeStart ;
set = > setLifetime ( value , LifetimeEnd ) ;
}
public override double LifetimeEnd
{
get = > base . LifetimeEnd ;
set = > setLifetime ( LifetimeStart , value ) ;
}
private void setLifetime ( double lifetimeStart , double lifetimeEnd )
{
base . LifetimeStart = lifetimeStart ;
base . LifetimeEnd = lifetimeEnd ;
if ( lifetimeEntry ! = null )
2019-07-16 12:45:59 +08:00
{
2020-11-10 19:16:52 +08:00
lifetimeEntry . LifetimeStart = lifetimeStart ;
lifetimeEntry . LifetimeEnd = lifetimeEnd ;
2019-07-16 12:45:59 +08:00
}
}
/// <summary>
/// A safe offset prior to the start time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> may begin displaying contents.
/// By default, <see cref="DrawableHitObject"/>s are assumed to display their contents within 10 seconds prior to the start time of <see cref="HitObject"/>.
/// </summary>
/// <remarks>
/// This is only used as an optimisation to delay the initial update of this <see cref="DrawableHitObject"/> and may be tuned more aggressively if required.
2019-07-22 15:08:38 +08:00
/// It is indirectly used to decide the automatic transform offset provided to <see cref="UpdateInitialTransforms"/>.
2019-09-12 18:30:27 +08:00
/// A more accurate <see cref="LifetimeStart"/> should be set for further optimisation (in <see cref="LoadComplete"/>, for example).
2020-11-16 20:47:33 +08:00
/// <para>
/// Only has an effect if this <see cref="DrawableHitObject"/> is not being pooled.
/// For pooled <see cref="DrawableHitObject"/>s, use <see cref="HitObjectLifetimeEntry.InitialLifetimeOffset"/> instead.
/// </para>
2019-07-16 12:45:59 +08:00
/// </remarks>
protected virtual double InitialLifetimeOffset = > 10000 ;
2020-11-04 15:04:15 +08:00
/// <summary>
/// The time at which state transforms should be applied that line up to <see cref="HitObject"/>'s StartTime.
/// This is used to offset calls to <see cref="UpdateStateTransforms"/>.
/// </summary>
2020-11-11 18:01:12 +08:00
public double StateUpdateTime = > HitObject . StartTime ;
2020-11-04 15:04:15 +08:00
/// <summary>
/// The time at which judgement dependent state transforms should be applied. This is equivalent of the (end) time of the object, in addition to any judgement offset.
/// This is used to offset calls to <see cref="UpdateHitStateTransforms"/>.
/// </summary>
2020-11-11 18:01:12 +08:00
public double HitStateUpdateTime = > Result ? . TimeAbsolute ? ? HitObject . GetEndTime ( ) ;
2020-11-04 15:04:15 +08:00
2019-07-16 12:45:59 +08:00
/// <summary>
2019-07-16 17:19:13 +08:00
/// Will be called at least once after this <see cref="DrawableHitObject"/> has become not alive.
2019-07-16 12:45:59 +08:00
/// </summary>
2019-07-16 17:19:13 +08:00
public virtual void OnKilled ( )
2019-07-16 12:45:59 +08:00
{
foreach ( var nested in NestedHitObjects )
2019-07-16 17:19:13 +08:00
nested . OnKilled ( ) ;
2020-10-05 14:07:46 +08:00
// failsafe to ensure looping samples don't get stuck in a playing state.
// this could occur in a non-frame-stable context where DrawableHitObjects get killed before a SkinnableSound has the chance to be stopped.
2020-10-05 15:24:02 +08:00
StopAllSamples ( ) ;
2020-09-29 14:07:55 +08:00
2019-07-16 12:45:59 +08:00
UpdateResult ( false ) ;
}
2020-11-25 22:38:47 +08:00
/// <summary>
/// The maximum offset from the end time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> can be judged.
/// The time offset of <see cref="Result"/> will be clamped to this value during <see cref="ApplyResult"/>.
/// <para>
/// Defaults to the miss window of <see cref="HitObject"/>.
/// </para>
/// </summary>
/// <remarks>
/// This does not affect the time offset provided to invocations of <see cref="CheckForResult"/>.
/// </remarks>
protected virtual double MaximumJudgementOffset = > HitObject . HitWindows ? . WindowFor ( HitResult . Miss ) ? ? 0 ;
2018-04-13 17:19:50 +08:00
/// <summary>
2018-08-06 09:55:38 +08:00
/// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as
/// the <see cref="ScoreProcessor"/> of the <see cref="JudgementResult"/>.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-08-06 09:55:38 +08:00
/// <param name="application">The callback that applies changes to the <see cref="JudgementResult"/>.</param>
2018-08-03 14:38:48 +08:00
protected void ApplyResult ( Action < JudgementResult > application )
2018-04-13 17:19:50 +08:00
{
2020-10-01 20:33:54 +08:00
if ( Result . HasResult )
2020-10-01 20:48:45 +08:00
throw new InvalidOperationException ( "Cannot apply result on a hitobject that already has a result." ) ;
2020-10-01 20:33:54 +08:00
2018-08-03 14:38:48 +08:00
application ? . Invoke ( Result ) ;
2018-08-02 19:35:54 +08:00
2018-08-06 10:07:05 +08:00
if ( ! Result . HasResult )
throw new InvalidOperationException ( $"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}." ) ;
2020-09-29 14:14:03 +08:00
// Some (especially older) rulesets use scorable judgements instead of the newer ignorehit/ignoremiss judgements.
2020-10-01 11:28:33 +08:00
// Can be removed 20210328
2020-09-29 14:14:03 +08:00
if ( Result . Judgement . MaxResult = = HitResult . IgnoreHit )
{
2020-10-01 11:28:33 +08:00
HitResult originalType = Result . Type ;
2020-09-29 14:14:03 +08:00
if ( Result . Type = = HitResult . Miss )
Result . Type = HitResult . IgnoreMiss ;
else if ( Result . Type > = HitResult . Meh & & Result . Type < = HitResult . Perfect )
Result . Type = HitResult . IgnoreHit ;
2020-10-01 11:28:33 +08:00
if ( Result . Type ! = originalType )
{
Logger . Log ( $"{GetType().ReadableName()} applied an invalid hit result ({originalType}) when {nameof(HitResult.IgnoreMiss)} or {nameof(HitResult.IgnoreHit)} is expected.\n"
+ $"This has been automatically adjusted to {Result.Type}, and support will be removed from 2020-03-28 onwards." , level : LogLevel . Important ) ;
}
2020-09-29 14:14:03 +08:00
}
if ( ! Result . Type . IsValidHitResult ( Result . Judgement . MinResult , Result . Judgement . MaxResult ) )
{
throw new InvalidOperationException (
$"{GetType().ReadableName()} applied an invalid hit result (was: {Result.Type}, expected: [{Result.Judgement.MinResult} ... {Result.Judgement.MaxResult}])." ) ;
}
2020-11-25 22:38:47 +08:00
Result . TimeOffset = Math . Min ( MaximumJudgementOffset , Time . Current - HitObject . GetEndTime ( ) ) ;
2018-04-13 17:19:50 +08:00
2020-10-03 14:09:10 +08:00
if ( Result . HasResult )
updateState ( Result . IsHit ? ArmedState . Hit : ArmedState . Miss ) ;
2018-04-13 17:19:50 +08:00
2018-08-06 09:54:16 +08:00
OnNewResult ? . Invoke ( this , Result ) ;
2018-04-13 17:19:50 +08:00
}
/// <summary>
2018-08-06 10:31:54 +08:00
/// Processes this <see cref="DrawableHitObject"/>, checking if a scoring result has occurred.
2018-04-13 17:19:50 +08:00
/// </summary>
/// <param name="userTriggered">Whether the user triggered this process.</param>
2018-08-06 10:31:54 +08:00
/// <returns>Whether a scoring result has occurred from this <see cref="DrawableHitObject"/> or any nested <see cref="DrawableHitObject"/>.</returns>
2018-08-06 10:31:46 +08:00
protected bool UpdateResult ( bool userTriggered )
2018-04-13 17:19:50 +08:00
{
2019-06-13 11:21:49 +08:00
// It's possible for input to get into a bad state when rewinding gameplay, so results should not be processed
if ( Time . Elapsed < 0 )
return false ;
2019-09-04 17:14:55 +08:00
if ( Judged )
2018-04-13 17:19:50 +08:00
return false ;
2020-11-25 22:38:47 +08:00
CheckForResult ( userTriggered , Time . Current - HitObject . GetEndTime ( ) ) ;
2018-04-13 17:19:50 +08:00
2019-09-04 17:14:55 +08:00
return Judged ;
2018-04-13 17:19:50 +08:00
}
/// <summary>
2018-08-06 10:31:54 +08:00
/// Checks if a scoring result has occurred for this <see cref="DrawableHitObject"/>.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-08-06 10:31:54 +08:00
/// <remarks>
/// If a scoring result has occurred, this method must invoke <see cref="ApplyResult"/> to update the result and notify responders.
/// </remarks>
2018-04-13 17:19:50 +08:00
/// <param name="userTriggered">Whether the user triggered this check.</param>
2018-08-06 10:31:54 +08:00
/// <param name="timeOffset">The offset from the end time of the <see cref="HitObject"/> at which this check occurred.
/// A <paramref name="timeOffset"/> > 0 implies that this check occurred after the end time of the <see cref="HitObject"/>. </param>
2018-08-06 10:31:46 +08:00
protected virtual void CheckForResult ( bool userTriggered , double timeOffset )
2018-04-13 17:19:50 +08:00
{
}
2018-08-02 19:35:54 +08:00
2018-08-06 09:55:38 +08:00
/// <summary>
/// Creates the <see cref="JudgementResult"/> that represents the scoring result for this <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="judgement">The <see cref="Judgement"/> that provides the scoring information.</param>
2019-09-02 16:14:40 +08:00
protected virtual JudgementResult CreateResult ( Judgement judgement ) = > new JudgementResult ( HitObject , judgement ) ;
2019-10-18 12:18:41 +08:00
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
2020-11-06 21:15:00 +08:00
if ( HitObject ! = null )
HitObject . DefaultsApplied - = onDefaultsApplied ;
2019-10-18 12:18:41 +08:00
}
2018-04-13 17:19:50 +08:00
}
public abstract class DrawableHitObject < TObject > : DrawableHitObject
where TObject : HitObject
{
2020-11-06 22:04:28 +08:00
public new TObject HitObject = > ( TObject ) base . HitObject ;
2018-04-13 17:19:50 +08:00
protected DrawableHitObject ( TObject hitObject )
: base ( hitObject )
{
}
}
}