using System;
using System.Collections.Generic;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
using UnityEngine.XR.Interaction.Toolkit.Interactors;
using UnityEngine.XR.Interaction.Toolkit.Utilities.Pooling;
namespace UnityEngine.XR.Interaction.Toolkit.Utilities
{
///
/// Use this class to maintain a registration of items (like Interactors or Interactables). This maintains
/// a synchronized list that stays constant until buffered registration status changes are
/// explicitly committed.
///
/// The type of object to register, such as or .
///
/// Items like Interactors and Interactables may be registered or unregistered (such as from an Interaction Manager)
/// at any time, including when processing those items. This class can be used to manage those registration changes.
/// For consistency with the functionality of Unity components which do not have
/// Update called the same frame in which they are enabled, disabled, or destroyed,
/// this class will maintain multiple lists to achieve that desired result with processing
/// the items, these lists are pooled and reused between instances.
///
abstract class BaseRegistrationList
{
///
/// Reusable list of buffered items (used to avoid unnecessary allocations when items to be added or removed
/// need to be monitored).
///
static readonly LinkedPool> s_BufferedListPool = new LinkedPool>
(() => new List(), actionOnRelease: list => list.Clear(), collectionCheck: false);
///
/// A snapshot of registered items that should potentially be processed this update phase of the current frame.
/// The count of items shall only change upon a call to .
///
///
/// Items being in this collection does not imply that the item is currently registered.
///
/// Logically this should be an but is kept as a
/// to avoid allocations when iterating. Use and
/// instead of directly changing this list.
///
public List registeredSnapshot { get; } = new List();
///
/// Gets the number of registered items including the effective result of buffered registration changes.
///
public int flushedCount => registeredSnapshot.Count - bufferedRemoveCount + bufferedAddCount;
///
/// List with buffered items to be added when calling .
/// The count of items shall only change upon a call to ,
/// or .
/// This list can be , use to check if there are elements
/// before directly accessing it. A new list is pooled from when needed.
///
///
/// Logically this should be an but is kept as a to avoid
/// allocations when iterating.
///
protected List m_BufferedAdd;
///
/// List with buffered items to be removed when calling .
/// The count of items shall only change upon a call to ,
/// or .
/// This list can be , use to check if there are elements
/// before directly accessing it. A new list is pooled from when needed.
///
///
/// Logically this should be an but is kept as a to avoid
/// allocations when iterating.
///
protected List m_BufferedRemove;
///
/// The number of buffered items to be added when calling .
///
protected int bufferedAddCount => m_BufferedAdd?.Count ?? 0;
///
/// The number of buffered items to be removed when calling .
///
protected int bufferedRemoveCount => m_BufferedRemove?.Count ?? 0;
///
/// Adds the given item to the list.
///
/// The item to be added.
///
/// Gets a new list from the if needed.
///
protected void AddToBufferedAdd(T item)
{
if (m_BufferedAdd == null)
m_BufferedAdd = s_BufferedListPool.Get();
m_BufferedAdd.Add(item);
}
///
/// Removes the given item from the list.
///
/// The item to be removed.
/// Returns if the item was successfully removed. Otherwise, returns .
protected bool RemoveFromBufferedAdd(T item) => m_BufferedAdd != null && m_BufferedAdd.Remove(item);
///
/// Removes all items from the and returns this list to the pool ().
///
protected void ClearBufferedAdd()
{
if (m_BufferedAdd == null)
return;
s_BufferedListPool.Release(m_BufferedAdd);
m_BufferedAdd = null;
}
///
/// Adds the given item to the list.
///
/// The item to be added.
///
/// Gets a new list from the if needed.
///
protected void AddToBufferedRemove(T item)
{
if (m_BufferedRemove == null)
m_BufferedRemove = s_BufferedListPool.Get();
m_BufferedRemove.Add(item);
}
///
/// Removes the given item from the list.
///
/// The item to be removed.
/// Returns if the item was successfully removed. Otherwise, returns .
protected bool RemoveFromBufferedRemove(T item) => m_BufferedRemove != null && m_BufferedRemove.Remove(item);
///
/// Removes all items from the and returns this list to the pool ().
///
protected void ClearBufferedRemove()
{
if (m_BufferedRemove == null)
return;
s_BufferedListPool.Release(m_BufferedRemove);
m_BufferedRemove = null;
}
///
/// Checks the registration status of .
///
/// The item to query.
/// Returns if registered. Otherwise, returns .
///
/// This includes pending changes that have not yet been pushed to .
///
///
public abstract bool IsRegistered(T item);
///
/// Faster variant of that assumes that the is in the snapshot.
/// It short circuits the check when there are no pending changes to unregister, which is usually the case.
///
/// The item to query.
/// Returns if registered.
///
/// This includes pending changes that have not yet been pushed to .
/// Use this method instead of when iterating over
/// for improved performance.
///
///
public abstract bool IsStillRegistered(T item);
///
/// Register .
///
/// The item to register.
/// Returns if a change in registration status occurred. Otherwise, returns .
public abstract bool Register(T item);
///
/// Unregister .
///
/// The item to unregister.
/// Returns if a change in registration status occurred. Otherwise, returns .
public abstract bool Unregister(T item);
///
/// Flush pending registration changes into .
///
public abstract void Flush();
///
/// Return all registered items into List in the order they were registered.
///
/// List to receive registered items.
///
/// Clears before adding to it.
///
public abstract void GetRegisteredItems(List results);
///
/// Returns the registered item at based on the order they were registered.
///
/// Index of the item to return. Must be smaller than and not negative.
/// Returns the item at the given index.
public abstract T GetRegisteredItemAt(int index);
///
/// Moves the given item in the registration list. Takes effect immediately without calling .
/// If the item is not in the registration list, this can be used to insert the item at the specified index.
///
/// The item to move or register.
/// New index of the item.
/// Returns if the item was registered as a result of this method, otherwise returns .
/// Throws when there are pending registration changes that have not been flushed.
public bool MoveItemImmediately(T item, int newIndex)
{
if (bufferedRemoveCount != 0 || bufferedAddCount != 0)
throw new InvalidOperationException("Cannot move item when there are pending registration changes that have not been flushed.");
var currentIndex = registeredSnapshot.IndexOf(item);
if (currentIndex == newIndex)
return false;
if (currentIndex >= 0)
registeredSnapshot.RemoveAt(currentIndex);
registeredSnapshot.Insert(newIndex, item);
OnItemMovedImmediately(item, newIndex);
return currentIndex < 0;
}
///
/// Called after the given item has been inserted at or moved to the specified index.
///
/// The item that was moved or registered.
/// New index of the item.
protected virtual void OnItemMovedImmediately(T item, int newIndex)
{
}
///
/// Unregister all currently registered items. Starts from the last registered item and proceeds backward
/// until the first registered item is unregistered.
///
public void UnregisterAll()
{
using (s_BufferedListPool.Get(out var registeredItems))
{
GetRegisteredItems(registeredItems);
for (var i = registeredItems.Count - 1; i >= 0; --i)
Unregister(registeredItems[i]);
}
}
protected static void EnsureCapacity(List list, int capacity)
{
if (list.Capacity < capacity)
list.Capacity = capacity;
}
}
///
sealed class RegistrationList : BaseRegistrationList
{
readonly HashSet m_UnorderedBufferedAdd = new HashSet();
readonly HashSet m_UnorderedBufferedRemove = new HashSet();
readonly HashSet m_UnorderedRegisteredSnapshot = new HashSet();
readonly HashSet m_UnorderedRegisteredItems = new HashSet();
///
/// Equivalent to m_UnorderedBufferedRemove.Count == 0.
///
///
/// This profiled slightly faster than checking the count directly, and is used by
/// which is called during each loop iteration for every registered item.
///
bool m_BufferedRemoveEmpty = true;
///
public override bool IsRegistered(T item) => m_UnorderedRegisteredItems.Contains(item);
///
public override bool IsStillRegistered(T item) => m_BufferedRemoveEmpty || !m_UnorderedBufferedRemove.Contains(item);
///
public override bool Register(T item)
{
if (m_UnorderedBufferedAdd.Count > 0 && m_UnorderedBufferedAdd.Contains(item))
return false;
var snapshotContainsItem = m_UnorderedRegisteredSnapshot.Contains(item);
if ((!m_BufferedRemoveEmpty && m_UnorderedBufferedRemove.Remove(item)) || !snapshotContainsItem)
{
RemoveFromBufferedRemove(item);
m_BufferedRemoveEmpty = m_UnorderedBufferedRemove.Count == 0;
m_UnorderedRegisteredItems.Add(item);
if (!snapshotContainsItem)
{
AddToBufferedAdd(item);
m_UnorderedBufferedAdd.Add(item);
}
return true;
}
return false;
}
///
public override bool Unregister(T item)
{
if (!m_BufferedRemoveEmpty && m_UnorderedBufferedRemove.Contains(item))
return false;
if (m_UnorderedBufferedAdd.Count > 0 && m_UnorderedBufferedAdd.Remove(item))
{
RemoveFromBufferedAdd(item);
m_UnorderedRegisteredItems.Remove(item);
return true;
}
if (m_UnorderedRegisteredSnapshot.Contains(item))
{
AddToBufferedRemove(item);
m_UnorderedBufferedRemove.Add(item);
m_BufferedRemoveEmpty = false;
m_UnorderedRegisteredItems.Remove(item);
return true;
}
return false;
}
///
public override void Flush()
{
// This method is called multiple times each frame,
// so additional explicit Count checks are done for
// performance.
if (!m_BufferedRemoveEmpty)
{
foreach (var item in m_BufferedRemove)
{
registeredSnapshot.Remove(item);
m_UnorderedRegisteredSnapshot.Remove(item);
}
ClearBufferedRemove();
m_UnorderedBufferedRemove.Clear();
m_BufferedRemoveEmpty = true;
}
if (bufferedAddCount > 0)
{
foreach (var item in m_BufferedAdd)
{
if (!m_UnorderedRegisteredSnapshot.Contains(item))
{
registeredSnapshot.Add(item);
m_UnorderedRegisteredSnapshot.Add(item);
}
}
ClearBufferedAdd();
m_UnorderedBufferedAdd.Clear();
}
}
///
public override void GetRegisteredItems(List results)
{
if (results == null)
throw new ArgumentNullException(nameof(results));
results.Clear();
EnsureCapacity(results, flushedCount);
foreach (var item in registeredSnapshot)
{
if (!m_BufferedRemoveEmpty && m_UnorderedBufferedRemove.Contains(item))
continue;
results.Add(item);
}
if (bufferedAddCount > 0)
results.AddRange(m_BufferedAdd);
}
///
public override T GetRegisteredItemAt(int index)
{
if (index < 0 || index >= flushedCount)
throw new ArgumentOutOfRangeException(nameof(index), "Index was out of range. Must be non-negative and less than the size of the registration collection.");
if (bufferedRemoveCount == 0 && bufferedAddCount == 0)
return registeredSnapshot[index];
if (index >= registeredSnapshot.Count - bufferedRemoveCount)
return m_BufferedAdd[index - (registeredSnapshot.Count - bufferedRemoveCount)];
var effectiveIndex = 0;
foreach (var item in registeredSnapshot)
{
if (m_UnorderedBufferedRemove.Contains(item))
continue;
if (effectiveIndex == index)
return registeredSnapshot[index];
++effectiveIndex;
}
// Unreachable code
throw new ArgumentOutOfRangeException(nameof(index), "Index was out of range. Must be non-negative and less than the size of the registration collection.");
}
///
protected override void OnItemMovedImmediately(T item, int newIndex)
{
base.OnItemMovedImmediately(item, newIndex);
m_UnorderedRegisteredItems.Add(item);
m_UnorderedRegisteredSnapshot.Add(item);
}
}
}