// 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.Diagnostics.CodeAnalysis; namespace osu.Game.Rulesets.Objects.Pooling { /// /// Manages a mapping between and /// internal class HitObjectEntryManager { /// /// All entries, including entries of the nested hit objects. /// public IEnumerable AllEntries => entryMap.Values; /// /// Invoked when a new is added to this .. /// The second parameter of the event is the parent hit object. /// public event Action? OnEntryAdded; /// /// Invoked when a is removed from this . /// The second parameter of the event is the parent hit object. /// public event Action? OnEntryRemoved; /// /// Provides the reverse mapping of for each entry. /// private readonly Dictionary entryMap = new Dictionary(); /// /// Stores the parent hit object for entries of the nested hit objects. /// /// /// The parent hit object of a pooled hit object may be non-pooled. /// In that case, no corresponding is stored in this . /// private readonly Dictionary parentMap = new Dictionary(); public void Add(HitObjectLifetimeEntry entry, HitObject? parent) { HitObject hitObject = entry.HitObject; if (entryMap.ContainsKey(hitObject)) throw new InvalidOperationException($@"The {nameof(HitObjectLifetimeEntry)} is already added to this {nameof(HitObjectEntryManager)}."); // Add the entry. entryMap[hitObject] = entry; // If the entry has a parent, set it and add the entry to the parent's children. if (parent != null) { parentMap[entry] = parent; if (entryMap.TryGetValue(parent, out var parentEntry)) parentEntry.NestedEntries.Add(entry); } hitObject.DefaultsApplied += onDefaultsApplied; OnEntryAdded?.Invoke(entry, parent); } public bool Remove(HitObjectLifetimeEntry entry) { if (entry is SyntheticHitObjectEntry) return false; HitObject hitObject = entry.HitObject; if (!entryMap.ContainsKey(hitObject)) throw new InvalidOperationException($@"The {nameof(HitObjectLifetimeEntry)} is not contained in this {nameof(HitObjectEntryManager)}."); entryMap.Remove(hitObject); // If the entry has a parent, unset it and remove the entry from the parents' children. if (parentMap.Remove(entry, out var parent) && entryMap.TryGetValue(parent, out var parentEntry)) parentEntry.NestedEntries.Remove(entry); // Remove all the entries' children. foreach (var childEntry in entry.NestedEntries) Remove(childEntry); hitObject.DefaultsApplied -= onDefaultsApplied; OnEntryRemoved?.Invoke(entry, parent); return true; } public bool TryGet(HitObject hitObject, [MaybeNullWhen(false)] out HitObjectLifetimeEntry entry) { return entryMap.TryGetValue(hitObject, out entry); } /// /// As nested hit objects are recreated, remove entries of the old nested hit objects. /// private void onDefaultsApplied(HitObject hitObject) { if (!entryMap.TryGetValue(hitObject, out var entry)) return; // Replace the entire list rather than clearing to prevent circular traversal later. var previousEntries = entry.NestedEntries; entry.NestedEntries = new List(); // Remove all the entries' children. At this point the parents' (this entries') children list has been reconstructed, so this does not cause upwards traversal. foreach (var nested in previousEntries) Remove(nested); } } }