// 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. using System; using System.Diagnostics; using osu.Framework.Graphics; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Pooling; namespace osu.Game.Rulesets.Objects.Pooling { /// <summary> /// A <see cref="PoolableDrawable"/> that is controlled by <see cref="Entry"/> to implement drawable pooling and replay rewinding. /// </summary> /// <typeparam name="TEntry">The <see cref="LifetimeEntry"/> type storing state and controlling this drawable.</typeparam> public abstract partial class PoolableDrawableWithLifetime<TEntry> : PoolableDrawable where TEntry : LifetimeEntry { private TEntry? entry; /// <summary> /// The entry holding essential state of this <see cref="PoolableDrawableWithLifetime{TEntry}"/>. /// </summary> /// <remarks> /// If a non-null value is set before loading is started, the entry is applied when the loading is completed. /// It is not valid to set an entry while this <see cref="PoolableDrawableWithLifetime{TEntry}"/> is loading. /// </remarks> public TEntry? Entry { get => entry; set { if (LoadState == LoadState.NotLoaded) entry = value; else if (value != null) Apply(value); else if (HasEntryApplied) free(); } } /// <summary> /// Whether <see cref="Entry"/> is applied to this <see cref="PoolableDrawableWithLifetime{TEntry}"/>. /// When an <see cref="Entry"/> is set during initialization, it is not applied until loading is completed. /// </summary> protected bool HasEntryApplied { get; private set; } public override double LifetimeStart { get => base.LifetimeStart; set { if (Entry == null && LifetimeStart != value) throw new InvalidOperationException($"Cannot modify lifetime of {nameof(PoolableDrawableWithLifetime<TEntry>)} when entry is not set"); if (Entry != null) Entry.LifetimeStart = value; } } public override double LifetimeEnd { get => base.LifetimeEnd; set { if (Entry == null && LifetimeEnd != value) throw new InvalidOperationException($"Cannot modify lifetime of {nameof(PoolableDrawableWithLifetime<TEntry>)} when entry is not set"); if (Entry != null) Entry.LifetimeEnd = value; } } public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; protected PoolableDrawableWithLifetime(TEntry? initialEntry = null) { Entry = initialEntry; } protected override void LoadAsyncComplete() { base.LoadAsyncComplete(); // Apply the initial entry. if (Entry != null && !HasEntryApplied) apply(Entry); } /// <summary> /// Applies a new entry to be represented by this drawable. /// If there is an existing entry applied, the entry will be replaced. /// </summary> public void Apply(TEntry entry) { if (LoadState == LoadState.Loading) throw new InvalidOperationException($"Cannot apply a new {nameof(TEntry)} while currently loading."); apply(entry); } protected sealed override void FreeAfterUse() { base.FreeAfterUse(); // We preserve the existing entry in case we want to move a non-pooled drawable between different parent drawables. if (HasEntryApplied && IsInPool) free(); } /// <summary> /// Invoked to apply a new entry to this drawable. /// </summary> protected virtual void OnApply(TEntry entry) { } /// <summary> /// Invoked to revert application of the entry to this drawable. /// </summary> protected virtual void OnFree(TEntry entry) { } private void apply(TEntry entry) { if (HasEntryApplied) free(); this.entry = entry; entry.LifetimeChanged += setLifetimeFromEntry; setLifetimeFromEntry(entry); OnApply(entry); HasEntryApplied = true; } private void free() { Debug.Assert(Entry != null && HasEntryApplied); OnFree(Entry); Entry.LifetimeChanged -= setLifetimeFromEntry; entry = null; base.LifetimeStart = double.MinValue; base.LifetimeEnd = double.MaxValue; HasEntryApplied = false; } private void setLifetimeFromEntry(LifetimeEntry entry) { Debug.Assert(entry == Entry); base.LifetimeStart = entry.LifetimeStart; base.LifetimeEnd = entry.LifetimeEnd; } } }