/* * SPDX-License-Identifier: AGPL-3.0-or-later * Copyright (C) 2025 Sergej Görzen * This file is part of OmiLAXR. */ using System; using System.Collections.Generic; using System.Linq; using OmiLAXR.TrackingBehaviours; using UnityEngine; using Object = UnityEngine.Object; namespace OmiLAXR.Composers { /// /// Abstract base class for statement composers that process tracking behaviors. /// Manages statement caching, composition, and delivery to endpoints. /// /// Type of tracking behavior this composer handles /// Type of statements. [DefaultExecutionOrder(-100)] public abstract class Composer : DataProviderPipelineComponent, IComposer where T : PipelineComponent, ITrackingBehaviour where TStatement : class, IStatement { /// /// Array of tracking behaviors this composer is processing /// [HideInInspector] public T[] trackingBehaviours; /// /// If true, statements are dropped when no handlers are registered. /// Prevents unbounded memory growth if endpoints are disabled. /// [Tooltip("If true, statements are dropped when no handlers are registered.")] public bool dropStatementsIfNoHandlers = true; /// /// Optional cap for queued statements when dropStatementsIfNoHandlers is false. /// 0 = unlimited (not recommended for benchmarks). /// [Tooltip("Optional cap for queued statements when dropStatementsIfNoHandlers is false. 0 = unlimited.")] [Min(0)] public int maxQueuedStatements = 0; /// /// Cache for storing statements with string keys /// private readonly Dictionary _statementCache = new Dictionary(); /// /// Cache for storing statements with integer keys /// private readonly Dictionary _statementCacheInt = new Dictionary(); /// /// Stores a statement in cache with string key /// public void StoreStatement(string key, TStatement statement) => _statementCache[key] = statement; /// /// Stores a statement in cache with integer key /// public void StoreStatement(int key, TStatement statement) => _statementCacheInt[key] = statement; public void StoreStatement(ITrackingBehaviour tb, GameObject target, TStatement statement) => StoreStatement(CombineHash(tb.GetHashCode(), target.GetHashCode()), statement); /// /// Retrieves a cached statement by string key. Optionally removes it from the cache. /// public TStatement RestoreStatement(string key, bool erase = false) { if (_statementCache.TryGetValue(key, out var statement)) { if (erase) _statementCache.Remove(key); return statement; } return null; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] private static int CombineHash(int h1, int h2) { #if UNITY_2021_2_OR_NEWER return System.HashCode.Combine(h1, h2); #else unchecked { return ((h1 << 5) + h1) ^ h2; } #endif } /// /// Retrieves a cached statement by integer key. Optionally removes it from the cache. /// public TStatement RestoreStatement(int key, bool erase = false) { if (_statementCacheInt.TryGetValue(key, out var statement)) { if (erase) _statementCacheInt.Remove(key); return statement; } return null; } /// /// Retrieves a cached statement by composite key. Optionally removes it from the cache. /// public TStatement RestoreStatement(ITrackingBehaviour tb, GameObject target, bool erase = false) { var key = CombineHash(tb.GetHashCode(), target.GetHashCode()); return RestoreStatement(key, erase); } /// /// Initializes the composer, finds tracking behaviors, and starts composition /// protected override void OnEnable() { base.OnEnable(); // Generate composer name from class name _name = GetType().Name.Replace("Composer", ""); // Find all tracking behaviors of the specified type trackingBehaviours = GetTrackingBehaviours(); // Start composing statements for each tracking behavior foreach (var trackingBehaviour in trackingBehaviours) Compose(trackingBehaviour); // Process any queued statements FlushWaitList(); } protected virtual void OnDisable() { // Prevent retaining large amounts of data between enable/disable cycles _waitList.Clear(); _statementCache.Clear(); _statementCacheInt.Clear(); } /// /// Gets author information for statements created by this composer /// public abstract Author GetAuthor(); public virtual string GetDataStandardVersion() => "1.0.0"; /// /// Cached composer name derived from class name /// private string _name; /// /// Returns the display name of this composer /// public virtual string GetName() => _name; /// /// Returns the logical grouping for this composer /// public virtual ComposerGroup GetGroup() => ComposerGroup.Other; /// /// Indicates if this is a higher-level composer that processes other composers' output /// public virtual bool IsHigherComposer => false; /// /// Event fired after a statement has been composed and is ready for delivery /// private event ComposerAction _afterComposed; /// /// Public event wrapper for handler registration. /// public event ComposerAction AfterComposed { add { _afterComposed += value; FlushWaitList(); } remove { _afterComposed -= value; } } /// /// Queue for statements waiting for event handlers to be registered /// private readonly List _waitList = new List(); /// /// Finds all tracking behaviors of specified type in the scene /// /// Whether to include inactive GameObjects protected static TB[] GetTrackingBehaviours(bool includeInactive = false) where TB : Object, ITrackingBehaviour => FindObjects(includeInactive); /// /// Obsolete: Use SendStatement(ITrackingBehaviour, IStatement) instead /// [Obsolete( "Use SendStatement(ITrackingBehaviour, IStatement) instead. Immediate is not needed anymore due efficient thread queue handling.", true)] protected void SendStatement(ITrackingBehaviour statementOwner, TStatement statement, bool immediate) => SendStatement(statementOwner, statement); /// /// Sends a composed statement for delivery to endpoints /// /// The tracking behavior that generated this statement /// The composed statement to send protected void SendStatement(ITrackingBehaviour statementOwner, TStatement statement) { if (!enabled) return; // Set ownership and composer information statement.SetOwner(statementOwner); // If no handlers registered, drop or queue statement for later if (_afterComposed == null) { if (dropStatementsIfNoHandlers) return; if (maxQueuedStatements > 0 && _waitList.Count >= maxQueuedStatements) return; _waitList.Add(statement); return; } // Send statement to registered handlers _afterComposed.Invoke(this, statement); } /// /// Obsolete: Use SendStatement(ITrackingBehaviour, IStatement) instead /// [Obsolete( "Use SendStatement(ITrackingBehaviour, IStatement) instead. Immediate is not needed anymore due efficient thread queue handling.", true)] protected void SendStatementImmediate(ITrackingBehaviour statementOwner, TStatement statement) => SendStatement(statementOwner, statement, immediate: true); /// /// Obsolete: Use SendStatement(ITrackingBehaviour, IStatement, bool) instead /// [Obsolete("Use SendStatement(ITrackingBehaviour, IStatement, bool) instead.")] protected void SendStatement(TStatement statement, bool immediate = false) { SendStatement(trackingBehaviours.First(), statement, immediate); } /// /// Obsolete: Use SendStatementImmediate(ITrackingBehaviour, IStatement) instead /// [Obsolete("Use SendStatementImmediate(ITrackingBehaviour, IStatement) instead.")] protected void SendStatementImmediate(TStatement statement) => SendStatement(statement, immediate: true); /// /// Implements composition logic for the specific tracking behavior type. /// Override this method to define how statements are created from tracking events. /// /// The tracking behavior to compose statements for protected abstract void Compose(T tb); /// /// Processes queued statements when event handlers become available /// private void FlushWaitList() { if (_waitList.Count == 0 || _afterComposed == null) return; for (var i = 0; i < _waitList.Count; i++) { _afterComposed.Invoke(this, _waitList[i]); } _waitList.Clear(); } } }