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
2018-07-17 14:48:51 +08:00
using System ;
2018-04-13 17:19:50 +08:00
using System.Collections.Generic ;
2018-07-17 14:48:51 +08:00
using System.Linq ;
2020-11-13 23:54:57 +08:00
using JetBrains.Annotations ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics ;
using osu.Game.Rulesets.Objects.Drawables ;
using osu.Framework.Allocation ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2018-07-17 14:48:51 +08:00
using osu.Framework.Extensions.IEnumerableExtensions ;
2020-11-13 23:54:57 +08:00
using osu.Framework.Graphics.Containers ;
using osu.Framework.Graphics.Pooling ;
2020-11-10 22:32:30 +08:00
using osu.Game.Rulesets.Judgements ;
2018-08-03 20:03:11 +08:00
using osu.Game.Rulesets.Mods ;
2020-11-10 22:32:30 +08:00
using osu.Game.Rulesets.Objects ;
2018-11-20 15:51:59 +08:00
using osuTK ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.UI
{
2020-11-13 23:54:57 +08:00
[Cached(typeof(IPooledHitObjectProvider))]
public abstract class Playfield : CompositeDrawable , IPooledHitObjectProvider
2018-04-13 17:19:50 +08:00
{
2020-11-10 22:32:30 +08:00
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is judged.
/// </summary>
public event Action < DrawableHitObject , JudgementResult > NewResult ;
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> judgement is reverted.
/// </summary>
public event Action < DrawableHitObject , JudgementResult > RevertResult ;
2020-11-20 16:24:09 +08:00
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is added.
/// </summary>
2020-11-20 17:00:00 +08:00
/// <remarks>
/// This event is also called for nested <see cref="DrawableHitObject"/>s.
/// </remarks>
2020-11-20 16:24:09 +08:00
public event Action < DrawableHitObject > DrawableHitObjectAdded ;
2018-04-13 17:19:50 +08:00
/// <summary>
2018-07-17 14:51:10 +08:00
/// The <see cref="DrawableHitObject"/> contained in this Playfield.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-09-21 13:35:50 +08:00
public HitObjectContainer HitObjectContainer = > hitObjectContainerLazy . Value ;
private readonly Lazy < HitObjectContainer > hitObjectContainerLazy ;
2018-04-13 17:19:50 +08:00
2018-09-21 13:02:32 +08:00
/// <summary>
/// A function that converts gamefield coordinates to screen space.
/// </summary>
public Func < Vector2 , Vector2 > GamefieldToScreenSpace = > HitObjectContainer . ToScreenSpace ;
2020-03-23 18:18:56 +08:00
/// <summary>
/// A function that converts screen space coordinates to gamefield.
/// </summary>
public Func < Vector2 , Vector2 > ScreenSpaceToGamefield = > HitObjectContainer . ToLocalSpace ;
2018-04-13 17:19:50 +08:00
/// <summary>
2018-07-17 14:51:10 +08:00
/// All the <see cref="DrawableHitObject"/>s contained in this <see cref="Playfield"/> and all <see cref="NestedPlayfields"/>.
2018-04-13 17:19:50 +08:00
/// </summary>
2020-09-22 17:17:04 +08:00
public IEnumerable < DrawableHitObject > AllHitObjects
{
get
{
if ( HitObjectContainer = = null )
return Enumerable . Empty < DrawableHitObject > ( ) ;
var enumerable = HitObjectContainer . Objects ;
if ( nestedPlayfields . IsValueCreated )
enumerable = enumerable . Concat ( NestedPlayfields . SelectMany ( p = > p . AllHitObjects ) ) ;
return enumerable ;
}
}
2018-07-17 14:51:10 +08:00
/// <summary>
/// All <see cref="Playfield"/>s nested inside this <see cref="Playfield"/>.
/// </summary>
2018-07-17 14:48:51 +08:00
public IEnumerable < Playfield > NestedPlayfields = > nestedPlayfields . IsValueCreated ? nestedPlayfields . Value : Enumerable . Empty < Playfield > ( ) ;
2018-04-13 17:19:50 +08:00
2018-07-17 14:51:10 +08:00
private readonly Lazy < List < Playfield > > nestedPlayfields = new Lazy < List < Playfield > > ( ) ;
2018-04-13 17:19:50 +08:00
2018-07-20 16:04:33 +08:00
/// <summary>
/// Whether judgements should be displayed by this and and all nested <see cref="Playfield"/>s.
/// </summary>
public readonly BindableBool DisplayJudgements = new BindableBool ( true ) ;
2018-04-13 17:19:50 +08:00
/// <summary>
2018-09-21 13:02:32 +08:00
/// Creates a new <see cref="Playfield"/>.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-09-21 13:02:32 +08:00
protected Playfield ( )
2018-04-13 17:19:50 +08:00
{
RelativeSizeAxes = Axes . Both ;
2018-09-21 13:35:50 +08:00
2020-11-10 22:32:30 +08:00
hitObjectContainerLazy = new Lazy < HitObjectContainer > ( ( ) = > CreateHitObjectContainer ( ) . With ( h = >
{
h . NewResult + = ( d , r ) = > NewResult ? . Invoke ( d , r ) ;
h . RevertResult + = ( d , r ) = > RevertResult ? . Invoke ( d , r ) ;
2020-11-20 16:24:09 +08:00
h . DrawableHitObjectAdded + = d = > DrawableHitObjectAdded ? . Invoke ( d ) ;
2020-11-12 17:32:20 +08:00
h . HitObjectUsageBegan + = o = > HitObjectUsageBegan ? . Invoke ( o ) ;
h . HitObjectUsageFinished + = o = > HitObjectUsageFinished ? . Invoke ( o ) ;
2020-11-10 22:32:30 +08:00
} ) ) ;
2018-04-13 17:19:50 +08:00
}
2020-04-01 12:31:17 +08:00
[Resolved(CanBeNull = true)]
2019-04-10 16:11:17 +08:00
private IReadOnlyList < Mod > mods { get ; set ; }
2018-08-03 20:03:11 +08:00
2018-04-13 17:19:50 +08:00
[BackgroundDependencyLoader]
2019-04-08 17:32:05 +08:00
private void load ( )
2018-04-13 17:19:50 +08:00
{
2019-03-08 14:01:45 +08:00
Cursor = CreateCursor ( ) ;
2019-05-07 12:23:09 +08:00
2019-03-08 14:01:45 +08:00
if ( Cursor ! = null )
2019-04-22 16:06:01 +08:00
{
// initial showing of the cursor will be handed by MenuCursorContainer (via DrawableRuleset's IProvideCursor implementation).
Cursor . Hide ( ) ;
2019-03-25 18:21:47 +08:00
AddInternal ( Cursor ) ;
2019-04-22 16:06:01 +08:00
}
2018-04-13 17:19:50 +08:00
}
/// <summary>
/// Performs post-processing tasks (if any) after all DrawableHitObjects are loaded into this Playfield.
/// </summary>
2018-07-17 14:48:51 +08:00
public virtual void PostProcess ( ) = > NestedPlayfields . ForEach ( p = > p . PostProcess ( ) ) ;
2018-04-13 17:19:50 +08:00
/// <summary>
/// Adds a DrawableHitObject to this Playfield.
/// </summary>
/// <param name="h">The DrawableHitObject to add.</param>
2020-11-10 22:32:30 +08:00
public virtual void Add ( DrawableHitObject h )
{
HitObjectContainer . Add ( h ) ;
OnHitObjectAdded ( h . HitObject ) ;
}
2018-04-13 17:19:50 +08:00
/// <summary>
/// Remove a DrawableHitObject from this Playfield.
/// </summary>
/// <param name="h">The DrawableHitObject to remove.</param>
2020-11-10 22:32:30 +08:00
public virtual bool Remove ( DrawableHitObject h )
{
if ( ! HitObjectContainer . Remove ( h ) )
return false ;
OnHitObjectRemoved ( h . HitObject ) ;
return false ;
}
/// <summary>
/// Invoked when a <see cref="HitObject"/> is added to this <see cref="Playfield"/>.
/// </summary>
/// <param name="hitObject">The added <see cref="HitObject"/>.</param>
protected virtual void OnHitObjectAdded ( HitObject hitObject )
{
}
/// <summary>
/// Invoked when a <see cref="HitObject"/> is removed from this <see cref="Playfield"/>.
/// </summary>
/// <param name="hitObject">The removed <see cref="HitObject"/>.</param>
protected virtual void OnHitObjectRemoved ( HitObject hitObject )
{
}
2020-11-13 17:54:49 +08:00
/// <summary>
/// The cursor currently being used by this <see cref="Playfield"/>. May be null if no cursor is provided.
/// </summary>
public GameplayCursorContainer Cursor { get ; private set ; }
/// <summary>
/// Provide a cursor which is to be used for gameplay.
/// </summary>
/// <remarks>
/// The default provided cursor is invisible when inside the bounds of the <see cref="Playfield"/>.
/// </remarks>
/// <returns>The cursor, or null to show the menu cursor.</returns>
protected virtual GameplayCursorContainer CreateCursor ( ) = > new InvisibleCursorContainer ( ) ;
/// <summary>
/// Registers a <see cref="Playfield"/> as a nested <see cref="Playfield"/>.
/// This does not add the <see cref="Playfield"/> to the draw hierarchy.
/// </summary>
/// <param name="otherPlayfield">The <see cref="Playfield"/> to add.</param>
protected void AddNested ( Playfield otherPlayfield )
{
otherPlayfield . DisplayJudgements . BindTo ( DisplayJudgements ) ;
otherPlayfield . NewResult + = ( d , r ) = > NewResult ? . Invoke ( d , r ) ;
otherPlayfield . RevertResult + = ( d , r ) = > RevertResult ? . Invoke ( d , r ) ;
otherPlayfield . HitObjectUsageBegan + = h = > HitObjectUsageBegan ? . Invoke ( h ) ;
otherPlayfield . HitObjectUsageFinished + = h = > HitObjectUsageFinished ? . Invoke ( h ) ;
nestedPlayfields . Value . Add ( otherPlayfield ) ;
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
// in the case a consumer forgets to add the HitObjectContainer, we will add it here.
if ( HitObjectContainer . Parent = = null )
AddInternal ( HitObjectContainer ) ;
}
protected override void Update ( )
{
base . Update ( ) ;
if ( mods ! = null )
{
foreach ( var mod in mods )
{
if ( mod is IUpdatableByPlayfield updatable )
updatable . Update ( this ) ;
}
}
}
/// <summary>
/// Creates the container that will be used to contain the <see cref="DrawableHitObject"/>s.
/// </summary>
protected virtual HitObjectContainer CreateHitObjectContainer ( ) = > new HitObjectContainer ( ) ;
2020-11-13 23:54:57 +08:00
#region Pooling support
private readonly Dictionary < Type , IDrawablePool > pools = new Dictionary < Type , IDrawablePool > ( ) ;
/// <summary>
/// Adds a <see cref="HitObjectLifetimeEntry"/> for a pooled <see cref="HitObject"/> to this <see cref="Playfield"/>.
/// </summary>
/// <param name="hitObject"></param>
public virtual void Add ( HitObject hitObject )
{
var entry = CreateLifetimeEntry ( hitObject ) ;
lifetimeEntryMap [ entry . HitObject ] = entry ;
HitObjectContainer . Add ( entry ) ;
OnHitObjectAdded ( entry . HitObject ) ;
}
/// <summary>
/// Removes a <see cref="HitObjectLifetimeEntry"/> for a pooled <see cref="HitObject"/> from this <see cref="Playfield"/>.
/// </summary>
/// <param name="hitObject"></param>
/// <returns>Whether the <see cref="HitObject"/> was successfully removed.</returns>
public virtual bool Remove ( HitObject hitObject )
{
if ( lifetimeEntryMap . Remove ( hitObject , out var entry ) )
{
HitObjectContainer . Remove ( entry ) ;
OnHitObjectRemoved ( hitObject ) ;
return true ;
}
bool removedFromNested = false ;
if ( nestedPlayfields . IsValueCreated )
removedFromNested = nestedPlayfields . Value . Any ( p = > p . Remove ( hitObject ) ) ;
return removedFromNested ;
}
/// <summary>
/// Creates the <see cref="HitObjectLifetimeEntry"/> for a given <see cref="HitObject"/>.
/// </summary>
/// <remarks>
/// This may be overridden to provide custom lifetime control (e.g. via <see cref="HitObjectLifetimeEntry.InitialLifetimeOffset"/>.
/// </remarks>
/// <param name="hitObject">The <see cref="HitObject"/> to create the entry for.</param>
/// <returns>The <see cref="HitObjectLifetimeEntry"/>.</returns>
[NotNull]
protected virtual HitObjectLifetimeEntry CreateLifetimeEntry ( [ NotNull ] HitObject hitObject ) = > new HitObjectLifetimeEntry ( hitObject ) ;
/// <summary>
/// Registers a default <see cref="DrawableHitObject"/> pool with this <see cref="DrawableRuleset"/> which is to be used whenever
/// <see cref="DrawableHitObject"/> representations are requested for the given <typeparamref name="TObject"/> type.
/// </summary>
/// <param name="initialSize">The number of <see cref="DrawableHitObject"/>s to be initially stored in the pool.</param>
/// <param name="maximumSize">
/// The maximum number of <see cref="DrawableHitObject"/>s that can be stored in the pool.
/// If this limit is exceeded, every subsequent <see cref="DrawableHitObject"/> will be created anew instead of being retrieved from the pool,
/// until some of the existing <see cref="DrawableHitObject"/>s are returned to the pool.
/// </param>
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
protected void RegisterPool < TObject , TDrawable > ( int initialSize , int? maximumSize = null )
where TObject : HitObject
where TDrawable : DrawableHitObject , new ( )
= > RegisterPool < TObject , TDrawable > ( new DrawablePool < TDrawable > ( initialSize , maximumSize ) ) ;
/// <summary>
/// Registers a custom <see cref="DrawableHitObject"/> pool with this <see cref="DrawableRuleset"/> which is to be used whenever
/// <see cref="DrawableHitObject"/> representations are requested for the given <typeparamref name="TObject"/> type.
/// </summary>
/// <param name="pool">The <see cref="DrawablePool{T}"/> to register.</param>
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
protected void RegisterPool < TObject , TDrawable > ( [ NotNull ] DrawablePool < TDrawable > pool )
where TObject : HitObject
where TDrawable : DrawableHitObject , new ( )
{
pools [ typeof ( TObject ) ] = pool ;
AddInternal ( pool ) ;
}
DrawableHitObject IPooledHitObjectProvider . GetPooledDrawableRepresentation ( HitObject hitObject )
{
var lookupType = hitObject . GetType ( ) ;
IDrawablePool pool ;
// Tests may add derived hitobject instances for which pools don't exist. Try to find any applicable pool and dynamically assign the type if the pool exists.
if ( ! pools . TryGetValue ( lookupType , out pool ) )
{
foreach ( var ( t , p ) in pools )
{
if ( ! t . IsInstanceOfType ( hitObject ) )
continue ;
pools [ lookupType ] = pool = p ;
break ;
}
}
2020-11-16 22:30:24 +08:00
return ( DrawableHitObject ) pool ? . Get ( d = >
2020-11-13 23:54:57 +08:00
{
var dho = ( DrawableHitObject ) d ;
// If this is the first time this DHO is being used (not loaded), then apply the DHO mods.
// This is done before Apply() so that the state is updated once when the hitobject is applied.
if ( ! dho . IsLoaded )
{
foreach ( var m in mods . OfType < IApplicableToDrawableHitObjects > ( ) )
m . ApplyToDrawableHitObjects ( dho . Yield ( ) ) ;
}
if ( ! lifetimeEntryMap . TryGetValue ( hitObject , out var entry ) )
lifetimeEntryMap [ hitObject ] = entry = CreateLifetimeEntry ( hitObject ) ;
dho . Apply ( hitObject , entry ) ;
} ) ;
}
#endregion
2020-11-13 17:53:37 +08:00
#region Editor logic
2020-11-13 17:54:49 +08:00
/// <summary>
/// Invoked when a <see cref="HitObject"/> becomes used by a <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// If this <see cref="HitObjectContainer"/> uses pooled objects, this represents the time when the <see cref="HitObject"/>s become alive.
/// </remarks>
internal event Action < HitObject > HitObjectUsageBegan ;
/// <summary>
/// Invoked when a <see cref="HitObject"/> becomes unused by a <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// If this <see cref="HitObjectContainer"/> uses pooled objects, this represents the time when the <see cref="HitObject"/>s become dead.
/// </remarks>
internal event Action < HitObject > HitObjectUsageFinished ;
2020-11-13 17:53:37 +08:00
private readonly Dictionary < HitObject , HitObjectLifetimeEntry > lifetimeEntryMap = new Dictionary < HitObject , HitObjectLifetimeEntry > ( ) ;
2020-11-12 17:30:32 +08:00
/// <summary>
/// Sets whether to keep a given <see cref="HitObject"/> always alive within this or any nested <see cref="Playfield"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to set.</param>
/// <param name="keepAlive">Whether to keep <paramref name="hitObject"/> always alive.</param>
2020-11-13 17:52:53 +08:00
internal void SetKeepAlive ( HitObject hitObject , bool keepAlive )
2020-11-12 17:30:32 +08:00
{
if ( lifetimeEntryMap . TryGetValue ( hitObject , out var entry ) )
{
entry . KeepAlive = keepAlive ;
return ;
}
if ( ! nestedPlayfields . IsValueCreated )
return ;
foreach ( var p in nestedPlayfields . Value )
p . SetKeepAlive ( hitObject , keepAlive ) ;
}
/// <summary>
/// Keeps all <see cref="HitObject"/>s alive within this and all nested <see cref="Playfield"/>s.
/// </summary>
2020-11-13 17:52:53 +08:00
internal void KeepAllAlive ( )
2020-11-12 17:30:32 +08:00
{
foreach ( var ( _ , entry ) in lifetimeEntryMap )
entry . KeepAlive = true ;
if ( ! nestedPlayfields . IsValueCreated )
return ;
foreach ( var p in nestedPlayfields . Value )
p . KeepAllAlive ( ) ;
}
2020-11-12 17:34:50 +08:00
/// <summary>
/// The amount of time prior to the current time within which <see cref="HitObject"/>s should be considered alive.
/// </summary>
2020-11-13 17:54:49 +08:00
internal double PastLifetimeExtension
2020-11-12 17:34:50 +08:00
{
get = > HitObjectContainer . PastLifetimeExtension ;
set
{
HitObjectContainer . PastLifetimeExtension = value ;
if ( ! nestedPlayfields . IsValueCreated )
return ;
foreach ( var nested in nestedPlayfields . Value )
nested . PastLifetimeExtension = value ;
}
}
/// <summary>
/// The amount of time after the current time within which <see cref="HitObject"/>s should be considered alive.
/// </summary>
2020-11-13 17:54:49 +08:00
internal double FutureLifetimeExtension
2020-11-12 17:34:50 +08:00
{
get = > HitObjectContainer . FutureLifetimeExtension ;
set
{
HitObjectContainer . FutureLifetimeExtension = value ;
if ( ! nestedPlayfields . IsValueCreated )
return ;
foreach ( var nested in nestedPlayfields . Value )
nested . FutureLifetimeExtension = value ;
}
}
2020-11-13 17:53:37 +08:00
#endregion
2019-11-29 16:35:11 +08:00
public class InvisibleCursorContainer : GameplayCursorContainer
{
protected override Drawable CreateCursor ( ) = > new InvisibleCursor ( ) ;
private class InvisibleCursor : Drawable
{
}
}
2018-04-13 17:19:50 +08:00
}
}