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 ;
using System.Linq ;
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 ;
2018-09-12 14:09:10 +08:00
using osu.Framework.Graphics.Primitives ;
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 ;
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
{
public readonly HitObject HitObject ;
/// <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
// Todo: Rulesets should be overriding the resources instead, but we need to figure out where/when to apply overrides first
protected virtual string SampleNamespace = > null ;
protected SkinnableSound Samples ;
2019-06-30 20:58:30 +08:00
protected virtual IEnumerable < HitSampleInfo > GetSamples ( ) = > HitObject . Samples ;
2018-04-13 17:19:50 +08:00
private readonly Lazy < List < DrawableHitObject > > nestedHitObjects = new Lazy < List < DrawableHitObject > > ( ) ;
2018-07-02 15:10:56 +08:00
public IEnumerable < DrawableHitObject > NestedHitObjects = > nestedHitObjects . IsValueCreated ? nestedHitObjects . Value : Enumerable . Empty < DrawableHitObject > ( ) ;
2018-04-13 17:19:50 +08:00
2018-08-06 09:54:16 +08:00
/// <summary>
/// Invoked when a <see cref="JudgementResult"/> has been applied by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
/// </summary>
public event Action < DrawableHitObject , JudgementResult > OnNewResult ;
/// <summary>
2018-08-06 11:29:22 +08:00
/// Invoked when a <see cref="JudgementResult"/> is being reverted by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
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
/// <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
2018-04-13 17:19:50 +08:00
private bool judgementOccurred ;
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 > ( ) ;
2019-07-23 20:08:41 +08:00
public IBindable < ArmedState > State = > state ;
2018-04-13 17:19:50 +08:00
protected DrawableHitObject ( HitObject hitObject )
{
HitObject = hitObject ;
}
[BackgroundDependencyLoader]
private void load ( )
{
2018-08-06 10:50:18 +08:00
var judgement = HitObject . CreateJudgement ( ) ;
2019-04-01 11:16:05 +08:00
2018-08-06 10:50:18 +08:00
if ( judgement ! = null )
2018-08-06 10:07:05 +08:00
{
2018-08-06 10:50:18 +08:00
Result = CreateResult ( judgement ) ;
2018-08-06 10:07:05 +08:00
if ( Result = = null )
2018-08-06 10:07:41 +08:00
throw new InvalidOperationException ( $"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}." ) ;
2018-08-06 10:07:05 +08:00
}
2018-04-13 17:19:50 +08:00
var samples = GetSamples ( ) . ToArray ( ) ;
2019-06-30 20:58:30 +08:00
if ( samples . Length > 0 )
2018-04-13 17:19:50 +08:00
{
if ( HitObject . SampleControlPoint = = null )
throw new ArgumentNullException ( nameof ( HitObject . SampleControlPoint ) , $"{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}." ) ;
2018-06-28 17:20:29 +08:00
samples = samples . Select ( s = > HitObject . SampleControlPoint . ApplyTo ( s ) ) . ToArray ( ) ;
foreach ( var s in samples )
s . Namespace = SampleNamespace ;
AddInternal ( Samples = new SkinnableSound ( samples ) ) ;
2018-04-13 17:19:50 +08:00
}
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2019-07-23 20:08:41 +08:00
updateState ( ArmedState . Idle , true ) ;
2019-07-22 14:05:56 +08:00
}
2019-07-22 14:33:12 +08:00
#region State / Transform Management
2019-07-22 15:08:38 +08:00
/// <summary>
/// Bind to apply a custom state which can override the default implementation.
/// </summary>
public event Action < DrawableHitObject , ArmedState > ApplyCustomUpdateState ;
2019-07-22 14:33:12 +08:00
/// <summary>
/// Enables automatic transform management of this hitobject. Implementation of transforms should be done in <see cref="UpdateInitialTransforms"/> and <see cref="UpdateStateTransforms"/> only. Rewinding and removing previous states is done automatically.
/// </summary>
/// <remarks>
/// Going forward, this is the preferred way of implementing <see cref="DrawableHitObject"/>s. Previous functionality
/// is offered as a compatibility layer until all rulesets have been migrated across.
/// </remarks>
2019-07-22 14:55:38 +08:00
protected virtual bool UseTransformStateManagement = > true ;
2019-07-22 14:05:56 +08:00
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 ;
// apply any custom state overrides
ApplyCustomUpdateState ? . Invoke ( this , newState ) ;
if ( newState = = ArmedState . Hit )
PlaySamples ( ) ;
2019-07-22 14:05:56 +08:00
if ( UseTransformStateManagement )
{
double transformTime = HitObject . StartTime - InitialLifetimeOffset ;
base . ApplyTransformsAt ( transformTime , true ) ;
base . ClearTransformsAfter ( transformTime , true ) ;
using ( BeginAbsoluteSequence ( transformTime , true ) )
{
2019-07-22 14:33:12 +08:00
UpdateInitialTransforms ( ) ;
2018-04-13 17:19:50 +08:00
2019-07-22 14:05:56 +08:00
var judgementOffset = Math . Min ( HitObject . HitWindows ? . HalfWindowFor ( HitResult . Miss ) ? ? double . MaxValue , Result ? . TimeOffset ? ? 0 ) ;
using ( BeginDelayedSequence ( InitialLifetimeOffset + judgementOffset , true ) )
{
2019-07-23 20:08:41 +08:00
UpdateStateTransforms ( newState ) ;
state . Value = newState ;
2019-07-22 14:05:56 +08:00
}
}
}
else
2019-07-23 20:08:41 +08:00
state . Value = newState ;
2019-07-22 14:05:56 +08:00
2019-07-23 20:08:41 +08:00
UpdateState ( newState ) ;
2018-04-13 17:19:50 +08:00
}
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.
/// </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-07-22 15:08:38 +08:00
/// <summary>
/// Apply transforms based on the current <see cref="ArmedState"/>. Previous states are automatically cleared.
/// </summary>
/// <param name="state">The new armed state.</param>
2019-07-22 14:33:12 +08:00
protected virtual void UpdateStateTransforms ( ArmedState state )
2019-07-22 14:05:56 +08:00
{
}
public override void ClearTransformsAfter ( double time , bool propagateChildren = false , string targetMember = null )
{
2019-07-23 20:15:55 +08:00
// When we are using automatic state management, parent calls to this should be blocked for safety.
2019-07-22 14:05:56 +08:00
if ( ! UseTransformStateManagement )
base . ClearTransformsAfter ( time , propagateChildren , targetMember ) ;
}
public override void ApplyTransformsAt ( double time , bool propagateChildren = false )
{
2019-07-23 20:15:55 +08:00
// When we are using automatic state management, parent calls to this should be blocked for safety.
2019-07-22 14:05:56 +08:00
if ( ! UseTransformStateManagement )
base . ApplyTransformsAt ( time , propagateChildren ) ;
}
2019-07-22 15:08:38 +08:00
/// <summary>
/// Legacy method to handle state changes.
/// Should generally not be used when <see cref="UseTransformStateManagement"/> is true; use <see cref="UpdateStateTransforms"/> instead.
/// </summary>
/// <param name="state">The new armed state.</param>
2019-07-22 14:05:56 +08:00
protected virtual void UpdateState ( ArmedState state )
{
}
2018-04-13 17:19:50 +08:00
2019-07-22 14:33:12 +08:00
#endregion
protected override void SkinChanged ( ISkinSource skin , bool allowFallback )
{
base . SkinChanged ( skin , allowFallback ) ;
if ( HitObject is IHasComboInformation combo )
AccentColour . Value = skin . GetValue < SkinConfiguration , Color4 ? > ( s = > s . ComboColours . Count > 0 ? s . ComboColours [ combo . ComboIndex % s . ComboColours . Count ] : ( Color4 ? ) null ) ? ? Color4 . White ;
}
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>
public void PlaySamples ( ) = > Samples ? . Play ( ) ;
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
{
2018-08-01 20:04:03 +08:00
var endTime = ( HitObject as IHasEndTime ) ? . EndTime ? ? HitObject . StartTime ;
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
}
}
2018-11-07 14:42:40 +08:00
protected override bool ComputeIsMaskedAway ( RectangleF maskingBounds ) = > AllJudged & & base . ComputeIsMaskedAway ( maskingBounds ) ;
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-07-16 12:45:59 +08:00
private double? lifetimeStart ;
public override double LifetimeStart
{
get = > lifetimeStart ? ? ( HitObject . StartTime - InitialLifetimeOffset ) ;
set
{
base . LifetimeStart = value ;
lifetimeStart = value ;
}
}
/// <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-07-16 12:45:59 +08:00
/// A more accurate <see cref="LifetimeStart"/> should be set inside <see cref="UpdateState"/> for an <see cref="ArmedState.Idle"/> state.
/// </remarks>
protected virtual double InitialLifetimeOffset = > 10000 ;
/// <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 ( ) ;
2019-07-16 12:45:59 +08:00
UpdateResult ( false ) ;
}
2018-04-13 17:19:50 +08:00
protected virtual void AddNested ( DrawableHitObject h )
{
2018-08-06 09:54:16 +08:00
h . OnNewResult + = ( d , r ) = > OnNewResult ? . Invoke ( d , r ) ;
2018-08-06 11:29:22 +08:00
h . OnRevertResult + = ( d , r ) = > OnRevertResult ? . Invoke ( d , r ) ;
2018-04-13 17:19:50 +08:00
h . ApplyCustomUpdateState + = ( d , j ) = > ApplyCustomUpdateState ? . Invoke ( d , j ) ;
nestedHitObjects . Value . Add ( h ) ;
}
/// <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
{
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)}." ) ;
2018-08-02 19:35:54 +08:00
judgementOccurred = true ;
2018-08-01 20:04:03 +08:00
2018-04-13 17:19:50 +08:00
// Ensure that the judgement is given a valid time offset, because this may not get set by the caller
var endTime = ( HitObject as IHasEndTime ) ? . EndTime ? ? HitObject . StartTime ;
2018-08-03 14:38:48 +08:00
Result . TimeOffset = Time . Current - endTime ;
2018-04-13 17:19:50 +08:00
2018-08-03 14:38:48 +08:00
switch ( Result . Type )
2018-04-13 17:19:50 +08:00
{
2018-08-03 14:38:48 +08:00
case HitResult . None :
break ;
2019-04-01 11:16:05 +08:00
2018-08-03 14:38:48 +08:00
case HitResult . Miss :
2019-07-23 20:08:41 +08:00
updateState ( ArmedState . Miss ) ;
2018-08-03 14:38:48 +08:00
break ;
2019-04-01 11:16:05 +08:00
2018-08-03 14:38:48 +08:00
default :
2019-07-23 20:08:41 +08:00
updateState ( ArmedState . Hit ) ;
2018-08-03 14:38:48 +08:00
break ;
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 ;
2018-04-13 17:19:50 +08:00
judgementOccurred = false ;
if ( AllJudged )
return false ;
2018-07-02 15:10:56 +08:00
foreach ( var d in NestedHitObjects )
2018-08-06 10:31:46 +08:00
judgementOccurred | = d . UpdateResult ( userTriggered ) ;
2018-04-13 17:19:50 +08:00
2018-08-01 20:04:03 +08:00
if ( judgementOccurred | | Judged )
2018-04-13 17:19:50 +08:00
return judgementOccurred ;
var endTime = ( HitObject as IHasEndTime ) ? . EndTime ? ? HitObject . StartTime ;
2018-08-06 10:31:46 +08:00
CheckForResult ( userTriggered , Time . Current - endTime ) ;
2018-04-13 17:19:50 +08:00
return judgementOccurred ;
}
/// <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>
2018-08-06 10:07:41 +08:00
protected virtual JudgementResult CreateResult ( Judgement judgement ) = > new JudgementResult ( judgement ) ;
2018-04-13 17:19:50 +08:00
}
public abstract class DrawableHitObject < TObject > : DrawableHitObject
where TObject : HitObject
{
public new readonly TObject HitObject ;
protected DrawableHitObject ( TObject hitObject )
: base ( hitObject )
{
HitObject = hitObject ;
}
}
}