// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable enable 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 { /// /// A that is controlled by to implement drawable pooling and replay rewinding. /// /// The type storing state and controlling this drawable. public abstract class PoolableDrawableWithLifetime : PoolableDrawable where TEntry : LifetimeEntry { private TEntry? entry; /// /// The entry holding essential state of this . /// /// /// 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 is loading. /// public TEntry? Entry { get => entry; set { if (LoadState == LoadState.NotLoaded) entry = value; else if (value != null) Apply(value); else if (HasEntryApplied) free(); } } /// /// Whether is applied to this . /// When an is set during initialization, it is not applied until loading is completed. /// 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)} 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)} 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); } /// /// Applies a new entry to be represented by this drawable. /// If there is an existing entry applied, the entry will be replaced. /// 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(); } /// /// Invoked to apply a new entry to this drawable. /// protected virtual void OnApply(TEntry entry) { } /// /// Invoked to revert application of the entry to this drawable. /// 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; } } }