// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Edit.Compose { /// /// Buffers events from the many s in a nested hierarchy. /// internal class HitObjectUsageEventBuffer : Component { /// /// Invoked when a becomes used by a . /// /// /// If the ruleset uses pooled objects, this represents the time when the s become alive. /// public event Action HitObjectUsageBegan; /// /// Invoked when a becomes unused by a . /// /// /// If the ruleset uses pooled objects, this represents the time when the s become dead. /// public event Action HitObjectUsageFinished; /// /// Invoked when a has been transferred to another . /// public event Action HitObjectUsageTransferred; private readonly Playfield playfield; /// /// Creates a new . /// /// The most top-level . public HitObjectUsageEventBuffer([NotNull] Playfield playfield) { this.playfield = playfield; playfield.HitObjectUsageBegan += onHitObjectUsageBegan; playfield.HitObjectUsageFinished += onHitObjectUsageFinished; } private readonly Dictionary pendingEvents = new Dictionary(); private void onHitObjectUsageBegan(HitObject hitObject) => updateEvent(hitObject, EventType.Began); private void onHitObjectUsageFinished(HitObject hitObject) => updateEvent(hitObject, EventType.Finished); private void updateEvent(HitObject hitObject, EventType newEvent) { if (!pendingEvents.TryGetValue(hitObject, out EventType existingEvent)) { pendingEvents[hitObject] = newEvent; return; } switch (existingEvent, newEvent) { // This exists as a safeguard to ensure that the sequence: { Began -> Finished }, where { ... } indicates a sequence within a single frame, does not trigger any events. // This is unlikely to occur in practice as it requires the usage to finish immediately after the HitObjectContainer updates hitobject lifetimes, // however, an Editor action scheduled somewhere between the lifetime update and this buffer's own Update() could cause this. case (EventType.Began, EventType.Finished): pendingEvents.Remove(hitObject); break; // This exists as a safeguard to ensure that the sequence: Began -> { Finished -> Began -> Finished }, where { ... } indicates a sequence within a single frame, // correctly leads into a final "finished" state rather than remaining in the intermediate "transferred" state. // As above, this is unlikely to occur in practice. case (EventType.Transferred, EventType.Finished): pendingEvents[hitObject] = EventType.Finished; break; case (EventType.Finished, EventType.Began): pendingEvents[hitObject] = EventType.Transferred; break; default: throw new ArgumentOutOfRangeException($"Unexpected event update ({existingEvent} => {newEvent})."); } } protected override void Update() { base.Update(); foreach (var (hitObject, e) in pendingEvents) { switch (e) { case EventType.Began: HitObjectUsageBegan?.Invoke(hitObject); break; case EventType.Transferred: HitObjectUsageTransferred?.Invoke(hitObject, playfield.AllHitObjects.Single(d => d.HitObject == hitObject)); break; case EventType.Finished: HitObjectUsageFinished?.Invoke(hitObject); break; } } pendingEvents.Clear(); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); playfield.HitObjectUsageBegan -= onHitObjectUsageBegan; playfield.HitObjectUsageFinished -= onHitObjectUsageFinished; } private enum EventType { /// /// A has started being used by a . /// Began, /// /// A has finished being used by a . /// Finished, /// /// An internal intermediate state that occurs when a has finished being used by one /// and started being used by another in the same frame. The may be the same instance in both cases. /// /// /// This usually occurs when a is transferred between s, /// but also occurs if the dies and becomes alive again in the same frame within the same . /// Transferred } } }