// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Game.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Skinning; using osuTK; using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Scoring; using osu.Framework.Extensions.ObjectExtensions; namespace osu.Game.Rulesets.UI { [Cached(typeof(IPooledHitObjectProvider))] [Cached(typeof(IPooledSampleProvider))] public abstract partial class Playfield : CompositeDrawable, IPooledHitObjectProvider, IPooledSampleProvider { /// /// Invoked when a is judged. /// public event Action NewResult; /// /// Invoked when a judgement result is reverted. /// public event Action RevertResult; /// /// The contained in this Playfield. /// public HitObjectContainer HitObjectContainer => hitObjectContainerLazy.Value; private readonly Lazy hitObjectContainerLazy; /// /// A function that converts gamefield coordinates to screen space. /// public Func GamefieldToScreenSpace => HitObjectContainer.ToScreenSpace; /// /// A function that converts screen space coordinates to gamefield. /// public Func ScreenSpaceToGamefield => HitObjectContainer.ToLocalSpace; /// /// All the s contained in this and all . /// public IEnumerable AllHitObjects { get { if (HitObjectContainer == null) return Enumerable.Empty(); var enumerable = HitObjectContainer.Objects; if (nestedPlayfields.Count != 0) enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects)); return enumerable; } } /// /// All s nested inside this . /// public IEnumerable NestedPlayfields => nestedPlayfields; private readonly List nestedPlayfields = new List(); /// /// Whether this is nested in another . /// public bool IsNested { get; private set; } /// /// Whether judgements should be displayed by this and and all nested s. /// public readonly BindableBool DisplayJudgements = new BindableBool(true); [Resolved(CanBeNull = true)] [CanBeNull] protected IReadOnlyList Mods { get; private set; } private readonly HitObjectEntryManager entryManager = new HitObjectEntryManager(); private readonly Stack judgementResults; /// /// Creates a new . /// protected Playfield() { RelativeSizeAxes = Axes.Both; hitObjectContainerLazy = new Lazy(() => CreateHitObjectContainer().With(h => { h.NewResult += onNewResult; h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); })); entryManager.OnEntryAdded += onEntryAdded; entryManager.OnEntryRemoved += onEntryRemoved; judgementResults = new Stack(); } [BackgroundDependencyLoader] private void load() { Cursor = CreateCursor(); if (Cursor != null) { // initial showing of the cursor will be handed by MenuCursorContainer (via DrawableRuleset's IProvideCursor implementation). Cursor.Hide(); AddInternal(Cursor); } } private void onNewDrawableHitObject(DrawableHitObject d) { d.OnNestedDrawableCreated += onNewDrawableHitObject; OnNewDrawableHitObject(d); Debug.Assert(!d.IsInitialized); d.IsInitialized = true; } /// /// Performs post-processing tasks (if any) after all DrawableHitObjects are loaded into this Playfield. /// public virtual void PostProcess() => NestedPlayfields.ForEach(p => p.PostProcess()); /// /// Adds a DrawableHitObject to this Playfield. /// /// The DrawableHitObject to add. public virtual void Add(DrawableHitObject h) { if (!h.IsInitialized) onNewDrawableHitObject(h); HitObjectContainer.Add(h); OnHitObjectAdded(h.HitObject); } /// /// Remove a DrawableHitObject from this Playfield. /// /// The DrawableHitObject to remove. public virtual bool Remove(DrawableHitObject h) { if (!HitObjectContainer.Remove(h)) return false; OnHitObjectRemoved(h.HitObject); return false; } /// /// Invoked when a is added to this . /// /// The added . protected virtual void OnHitObjectAdded(HitObject hitObject) { preloadSamples(hitObject); } /// /// Invoked when a is removed from this . /// /// The removed . protected virtual void OnHitObjectRemoved(HitObject hitObject) { } /// /// Invoked before a new is added to this . /// It is invoked only once even if the drawable is pooled and used multiple times for different s. /// /// /// This is also invoked for nested s. /// protected virtual void OnNewDrawableHitObject(DrawableHitObject drawableHitObject) { } /// /// The cursor currently being used by this . May be null if no cursor is provided. /// [CanBeNull] public GameplayCursorContainer Cursor { get; private set; } /// /// Provide a cursor which is to be used for gameplay. /// /// The cursor, or null to show the menu cursor. protected virtual GameplayCursorContainer CreateCursor() => null; /// /// Registers a as a nested . /// This does not add the to the draw hierarchy. /// /// The to add. protected void AddNested(Playfield otherPlayfield) { otherPlayfield.IsNested = true; otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements); otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r); otherPlayfield.RevertResult += r => RevertResult?.Invoke(r); otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h); otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h); nestedPlayfields.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 (!IsNested && Mods != null) { foreach (var mod in Mods) { if (mod is IUpdatableByPlayfield updatable) updatable.Update(this); } } // When rewinding, revert future judgements in the reverse order. while (judgementResults.Count > 0 && Time.Current < judgementResults.Peek().Time) revertResult(judgementResults.Pop()); } /// /// Creates the container that will be used to contain the s. /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); #region Pooling support private readonly Dictionary pools = new Dictionary(); /// /// Adds a for a pooled to this . /// /// public virtual void Add(HitObject hitObject) { var entry = CreateLifetimeEntry(hitObject); entryManager.Add(entry, null); } private void preloadSamples(HitObject hitObject) { // prepare sample pools ahead of time so we're not initialising at runtime. foreach (var sample in hitObject.Samples) prepareSamplePool(hitObject.SampleControlPoint.ApplyTo(sample)); foreach (var sample in hitObject.AuxiliarySamples) prepareSamplePool(hitObject.SampleControlPoint.ApplyTo(sample)); foreach (var nestedObject in hitObject.NestedHitObjects) preloadSamples(nestedObject); } /// /// Removes a for a pooled from this . /// /// /// Whether the was successfully removed. public virtual bool Remove(HitObject hitObject) { if (entryManager.TryGet(hitObject, out var entry)) { entryManager.Remove(entry); return true; } return nestedPlayfields.Any(p => p.Remove(hitObject)); } private void onEntryAdded(HitObjectLifetimeEntry entry, [CanBeNull] HitObject parentHitObject) { if (parentHitObject != null) return; HitObjectContainer.Add(entry); OnHitObjectAdded(entry.HitObject); } private void onEntryRemoved(HitObjectLifetimeEntry entry, [CanBeNull] HitObject parentHitObject) { if (parentHitObject != null) return; HitObjectContainer.Remove(entry); OnHitObjectRemoved(entry.HitObject); } /// /// Creates the for a given . /// /// /// This may be overridden to provide custom lifetime control (e.g. via . /// /// The to create the entry for. /// The . [NotNull] protected virtual HitObjectLifetimeEntry CreateLifetimeEntry([NotNull] HitObject hitObject) => new HitObjectLifetimeEntry(hitObject); /// /// Registers a default pool with this which is to be used whenever /// representations are requested for the given type. /// /// The number of s to be initially stored in the pool. /// /// The maximum number of s that can be stored in the pool. /// If this limit is exceeded, every subsequent will be created anew instead of being retrieved from the pool, /// until some of the existing s are returned to the pool. /// /// The type. /// The receiver for s. public void RegisterPool(int initialSize, int? maximumSize = null) where TObject : HitObject where TDrawable : DrawableHitObject, new() => RegisterPool(new DrawablePool(initialSize, maximumSize)); /// /// Registers a custom pool with this which is to be used whenever /// representations are requested for the given type. /// /// The to register. /// The type. /// The receiver for s. protected void RegisterPool([NotNull] DrawablePool pool) where TObject : HitObject where TDrawable : DrawableHitObject, new() { pools[typeof(TObject)] = pool; AddInternal(pool); } DrawableHitObject IPooledHitObjectProvider.GetPooledDrawableRepresentation(HitObject hitObject, DrawableHitObject parent) { var pool = prepareDrawableHitObjectPool(hitObject); return (DrawableHitObject)pool?.Get(d => { var dho = (DrawableHitObject)d; if (!dho.IsInitialized) { onNewDrawableHitObject(dho); // If this is the first time this DHO is being used, then apply the DHO mods. // This is done before Apply() so that the state is updated once when the hitobject is applied. if (Mods != null) { foreach (var m in Mods.OfType()) m.ApplyToDrawableHitObject(dho); } } if (!entryManager.TryGet(hitObject, out var entry)) { entry = CreateLifetimeEntry(hitObject); entryManager.Add(entry, parent?.HitObject); } dho.ParentHitObject = parent; dho.Apply(entry); }); } private IDrawablePool prepareDrawableHitObjectPool(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; } } return pool; } private readonly Dictionary> samplePools = new Dictionary>(); public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) => prepareSamplePool(sampleInfo).Get(); private DrawablePool prepareSamplePool(ISampleInfo sampleInfo) { if (samplePools.TryGetValue(sampleInfo, out var pool)) return pool; AddInternal(samplePools[sampleInfo] = pool = new DrawableSamplePool(sampleInfo, 1)); return pool; } private partial class DrawableSamplePool : DrawablePool { private readonly ISampleInfo sampleInfo; public DrawableSamplePool(ISampleInfo sampleInfo, int initialSize, int? maximumSize = null) : base(initialSize, maximumSize) { this.sampleInfo = sampleInfo; } protected override PoolableSkinnableSample CreateNewDrawable() => base.CreateNewDrawable().With(d => d.Apply(sampleInfo)); } #endregion private void onNewResult(DrawableHitObject drawable, JudgementResult result) { // Not using result.TimeAbsolute because that might change and also there is a potential precision issue. judgementResults.Push(new JudgementResultEntry(Time.Current, drawable.Entry.AsNonNull(), result)); NewResult?.Invoke(drawable, result); } private void revertResult(JudgementResultEntry entry) { var result = entry.Result; RevertResult?.Invoke(result); result.TimeOffset = 0; result.Type = HitResult.None; entry.HitObjectEntry.OnRevertResult(); } #region Editor logic /// /// Invoked when a becomes used by a . /// /// /// If this uses pooled objects, this represents the time when the s become alive. /// internal event Action HitObjectUsageBegan; /// /// Invoked when a becomes unused by a . /// /// /// If this uses pooled objects, this represents the time when the s become dead. /// internal event Action HitObjectUsageFinished; /// /// Sets whether to keep a given always alive within this or any nested . /// /// The to set. /// Whether to keep always alive. internal void SetKeepAlive(HitObject hitObject, bool keepAlive) { if (entryManager.TryGet(hitObject, out var entry)) { entry.KeepAlive = keepAlive; return; } foreach (var p in nestedPlayfields) p.SetKeepAlive(hitObject, keepAlive); } /// /// Keeps all s alive within this and all nested s. /// internal void KeepAllAlive() { foreach (var entry in entryManager.AllEntries) entry.KeepAlive = true; foreach (var p in nestedPlayfields) p.KeepAllAlive(); } /// /// The amount of time prior to the current time within which s should be considered alive. /// internal double PastLifetimeExtension { get => HitObjectContainer.PastLifetimeExtension; set { HitObjectContainer.PastLifetimeExtension = value; foreach (var nested in nestedPlayfields) nested.PastLifetimeExtension = value; } } /// /// The amount of time after the current time within which s should be considered alive. /// internal double FutureLifetimeExtension { get => HitObjectContainer.FutureLifetimeExtension; set { HitObjectContainer.FutureLifetimeExtension = value; foreach (var nested in nestedPlayfields) nested.FutureLifetimeExtension = value; } } #endregion } }