/* * 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 System.Reflection; using OmiLAXR.Composers; using OmiLAXR.Endpoints; using OmiLAXR.Listeners; using OmiLAXR.Filters; using OmiLAXR.TrackingBehaviours; using UnityEngine; using Object = UnityEngine.Object; namespace OmiLAXR { /// /// Core orchestration component for the OmiLAXR learning analytics pipeline system. /// Manages the complete lifecycle of object detection, filtering, tracking, and analytics generation. /// Coordinates between listeners, filters, tracking behaviors, and data providers to create /// a comprehensive learning analytics solution for Unity applications. /// [AddComponentMenu("OmiLAXR / Core / Pipeline")] [DefaultExecutionOrder(0)] // Execute at standard time, after extensions and settings public class Pipeline : MonoBehaviour { /// /// Memory for holding running pipelines. /// private static readonly List RunningPipelines = new List(); /// /// Amount of running pipelines. /// public static int RunningPipelinesCount => RunningPipelines.Count; /// /// Is true, if this pipeline is the last running. /// public bool IsLastRunningPipeline => RunningPipelinesCount == 1 && RunningPipelines[0].Equals(this); /// /// Indicates whether the pipeline is currently active and processing objects. /// Based on the GameObject's active state in the scene hierarchy. /// public bool IsRunning => gameObject.activeSelf; /// /// Reference to the Actor component representing the learner being tracked. /// Can be an individual Actor or an ActorGroup for multi-user scenarios. /// public Actor actor; /// /// Reference to the Instructor component that manages this pipeline's learning context. /// Provides oversight and coordination for analytics generation and delivery. /// public Instructor instructor; /// /// Collection of Listener components that detect objects in the scene. /// Listeners form the first stage of the pipeline by finding trackable objects. /// public readonly List Listeners = new List(); /// /// Collection of DataProvider components that handle analytics data processing. /// DataProviders compose and send learning analytics statements to configured endpoints. /// public readonly List DataProviders = new List(); /// /// Collection of TrackingBehaviour components that monitor detected objects. /// TrackingBehaviours generate events and gather data about learner interactions. /// public readonly List TrackingBehaviours = new List(); /// /// Collection of Filter components that refine the set of tracked objects. /// Filters remove unwanted objects and focus tracking on relevant interactions. /// public readonly List Filters = new List(); /// /// Dictionary mapping action names to their associated tracking behavior events. /// Built dynamically from ActionAttribute annotations on tracking behavior events. /// public readonly Dictionary> Actions = new Dictionary>(); /// /// Dictionary mapping gesture names to their associated tracking behavior events. /// Built dynamically from GestureAttribute annotations on tracking behavior events. /// public readonly Dictionary> Gestures = new Dictionary>(); /// /// Collection of pipeline extensions that have been applied to this pipeline. /// Extensions provide modular enhancements without modifying the core pipeline code. /// public readonly List Extensions = new List(); /// /// Array of ActorDataProvider components specific to this pipeline's actor. /// Cached during initialization for efficient access to actor-specific data providers. /// public ActorDataProvider[] ActorDataProviders { get; protected set; } /// /// Finds the first pipeline of the specified type in the scene. /// Uses Unity version-appropriate methods for optimal compatibility. /// /// Type of pipeline to find /// First pipeline instance of the specified type, or null if none found #if UNITY_2023_1_OR_NEWER public static T GetPipeline() where T : Pipeline => FindFirstObjectByType(); #else public static T GetPipeline() where T : Pipeline => FindObjectOfType(); #endif /// /// Finds the pipelines of any type in the scene. /// Uses Unity version-appropriate methods for optimal compatibility. /// /// All pipelines found, or null if none exist #if UNITY_2023_1_OR_NEWER public static Pipeline[] GetAll() => FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); #else public static Pipeline[] GetAll() => FindObjectsOfType(); #endif /// /// Finds the only active pipelines of any type in the scene. /// Uses Unity version-appropriate methods for optimal compatibility. /// /// All active pipelines found, or null if none exist public static Pipeline[] GetAllRunningPipelines() => RunningPipelines.ToArray(); /// /// Gets the first DataProvider of the specified type from this pipeline's collection. /// Provides type-safe access to specific data provider implementations. /// /// Type of DataProvider to find /// First DataProvider of the specified type, or null if none found public T GetDataProvider() where T : DataProvider => DataProviders.OfType().Select(dp => dp).FirstOrDefault(); /// /// Gets the first TrackingBehaviour of the specified type from this pipeline's collection. /// Provides type-safe access to specific tracking behavior implementations. /// /// Type of TrackingBehaviour to find (must be a PipelineComponent) /// First TrackingBehaviour of the specified type, or null if none found public T GetTrackingBehaviour() where T : PipelineComponent, ITrackingBehaviour => TrackingBehaviours.OfType().Select(dp => dp).FirstOrDefault(); /// /// Gets the first Filter of the specified type from this pipeline's collection. /// Provides type-safe access to specific filter implementations. /// /// Type of Filter to find /// First Filter of the specified type, or null if none found public T GetFilters() where T : Filter => Filters.OfType().Select(dp => dp).FirstOrDefault(); /// /// Gets the first Listener of the specified type from this pipeline's collection. /// Provides type-safe access to specific listener implementations. /// /// Type of Listener to find /// First Listener of the specified type, or null if none found public T GetListener() where T : Listener => Listeners.OfType().Select(listener => listener).FirstOrDefault(); /// Event fired after pipeline initialization is complete but before startup. public event PipelineInitHandler AfterInit; /// Event fired immediately before the pipeline starts processing objects. public event PipelineStartedHandler BeforeStartedPipeline; /// Event fired immediately after the pipeline has started processing objects. public event PipelineStartedHandler AfterStartedPipeline; /// Event fired during the collection phase of pipeline initialization. public event PipelineInitHandler OnCollect; /// Event fired when listeners find objects, before filtering is applied. public event Action AfterFoundObjects; /// Event fired after filters have processed the found objects. public event Action AfterFilteredObjects; /// Event fired when a composer creates an analytics statement. public event ComposerAction AfterComposedStatement; /// Event fired before an endpoint sends a statement to its destination. public event EndpointAction BeforeSendStatement; /// Event fired after an endpoint successfully sends a statement. public event EndpointAction AfterSendStatement; /// Event fired immediately before the pipeline stops processing objects. public event PipelineStoppedHandler BeforeStoppedPipeline; /// Event fired immediately after the pipeline has stopped processing objects. public event PipelineStoppedHandler AfterStoppedPipeline; public event PipelineStoppedHandler OnQuit; /// /// Collection of objects currently being tracked by the pipeline. /// Populated by listeners and filtered through the pipeline's filter chain. /// public readonly List TrackingObjects = new List(); /// /// Flag to prevent multiple cleanup calls during shutdown sequences. /// Ensures cleanup operations are performed exactly once per lifecycle. /// private bool _cleanupCalled; /// /// Flag to indicate that the application is quitting. /// Used to prevent pipeline cleanup during application shutdown. /// private bool _isQuittingApplication; /// /// Flag to prevent multiple startup calls during initialization sequences. /// Ensures startup operations are performed exactly once per lifecycle. /// private bool _startupCalled; /// /// Adds a pipeline component to the appropriate collection based on its type. /// Automatically categorizes components and handles extension registration. /// /// PipelineComponent to add to the pipeline public void Add(PipelineComponent comp) { var type = comp.GetType(); // Add to appropriate collection based on component type if (type.IsSubclassOf(typeof(Listener))) { if (!Listeners.Contains(comp)) Listeners.Add(comp as Listener); } else if (type.IsSubclassOf(typeof(Filter))) { if (!Filters.Contains(comp)) Filters.Add(comp as Filter); } else if (type.IsSubclassOf(typeof(ITrackingBehaviour))) { var tb = comp as ITrackingBehaviour; if (!TrackingBehaviours.Contains(tb)) TrackingBehaviours.Add(tb); } else if (type.IsSubclassOf(typeof(IPipelineExtension))) { // Handle pipeline extensions with special registration logic var pc = comp as IPipelineComponent; if (pc != null) { if (!Extensions.Contains(pc)) { var ext = pc as IPipelineExtension; ext?.Extend(this); // Apply extension to this pipeline Extensions.Add(ext); } } } } /// /// Finds the Actor component associated with this pipeline. /// Checks for ActorGroup first, then falls back to individual Actor. /// /// Actor or ActorGroup component, or null if none found private Actor FindActor() { var actorGroup = GetComponent(); return actorGroup ?? GetComponent(); } /// /// Initializes the pipeline by discovering and configuring all components. /// Sets up event bindings, collects components, and prepares for operation. /// Called once during the pipeline's lifetime before startup. /// private void Init() { // Ensure we have an actor for this pipeline if (actor == null) actor = FindActor(); // Cache actor-specific data providers for efficient access ActorDataProviders = GetComponentsInChildren(false); // Discover and collect all pipeline components from hierarchy TrackingBehaviours.AddRange(GetComponentsInChildren(false)); Listeners.AddRange(GetComponentsInChildren(false)); Filters.AddRange(GetComponentsInChildren(false)); // Find all available data providers in the scene (not just children) #if UNITY_2023_1_OR_NEWER DataProviders.AddRange(FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None)); #else DataProviders.AddRange(FindObjectsOfType(false)); #endif // Bind pipeline events to data provider events for analytics processing foreach (var dp in DataProviders) { // Connect composer events for statement creation foreach (var c in dp.Composers) { c.AfterComposed += AfterComposed; } // Connect endpoint events for statement delivery foreach (var ep in dp.Endpoints) { ep.OnSendingStatement += OnSendingStatement; ep.OnSentStatement += OnSentStatement; } } // Calculate component counts for logging var composersCount = DataProviders.Aggregate(0, (i, provider) => i + provider.Composers.Count); var hooksCount = DataProviders.Aggregate(0, (i, provider) => i + provider.Hooks.Count); // Notify interested parties that collection phase is beginning OnCollect?.Invoke(this); // Log initialization summary for debugging Log($"Initialized with {Listeners.Count} listeners, {Filters.Count} filters, {TrackingBehaviours.Count} tracking behaviours, {composersCount} composers, {hooksCount} hooks and {DataProviders.Count} data providers" ); // Notify that initialization is complete AfterInit?.Invoke(this); } /// /// Event handler for composer statement creation events. /// Forwards the event to pipeline subscribers for processing. /// /// Composer that created the statement /// Statement that was composed private void AfterComposed(IComposer composer, IStatement statement) => AfterComposedStatement?.Invoke(composer, statement); /// /// Event handler for endpoint statement sending events. /// Forwards the event to pipeline subscribers before delivery. /// /// Endpoint that will send the statement /// Statement being sent private void OnSendingStatement(Endpoint endpoint, IStatement statement) => BeforeSendStatement?.Invoke(endpoint, statement); /// /// Event handler for endpoint statement sent events. /// Forwards the event to pipeline subscribers after successful delivery. /// /// Endpoint that sent the statement /// Statement that was sent private void OnSentStatement(Endpoint endpoint, IStatement statement) => AfterSendStatement?.Invoke(endpoint, statement); /// /// Analyzes tracking behaviors to build action and gesture event mappings. /// Uses reflection to discover events with ActionAttribute and GestureAttribute annotations. /// Called during pipeline initialization to enable event categorization and filtering. /// private void CollectGesturesAndActions() { var tbs = TrackingBehaviours.ToArray(); Actions.Clear(); Gestures.Clear(); // Process each tracking behavior for annotated events foreach (var ts in tbs) { var properties = ts.GetTrackingBehaviourEvents(); foreach (var p in properties) { var ev = p.GetValue(ts) as ITrackingBehaviourEvent; var actionAttrs = p.GetCustomAttributes(); var gestureAttrs = p.GetCustomAttributes(); // Collect action-attributed events foreach (var actionAttr in actionAttrs) { if (Actions.ContainsKey(actionAttr.Name)) Actions[actionAttr.Name].Add(ev); else Actions.Add(actionAttr.Name, new List() { ev }); } // Collect gesture-attributed events foreach (var gestureAttr in gestureAttrs) { if (Gestures.ContainsKey(gestureAttr.Name)) Gestures[gestureAttr.Name].Add(ev); else Gestures.Add(gestureAttr.Name, new List() { ev }); } } } } /// /// Logs a formatted message with pipeline context information. /// Automatically includes the pipeline type name for easier debugging. /// /// Format string for the log message /// Parameters for string formatting protected void Log(string message, params object[] ps) => DebugLog.OmiLAXR.Print($"({GetType().Name}) " + message, ps); /// /// Unity OnEnable callback that triggers pipeline startup. /// Called when the GameObject becomes active in the scene. /// private void OnEnable() { ApplicationShutdownManager.Register(this); Startup(); } /// /// Starts the pipeline operation by initializing components and beginning object tracking. /// Ensures startup operations are performed only once per lifecycle. /// Connects listener events and begins the object detection and tracking process. /// private void Startup() { // Prevent duplicate startup calls if (_startupCalled) return; // Initialize pipeline if not already done Init(); _startupCalled = true; // Build action/gesture mappings from tracking behaviors CollectGesturesAndActions(); TrackingObjects.Clear(); // Start all enabled listeners to begin object detection foreach (var listener in Listeners) { listener.OnFoundObjects += FoundObjects; // Connect event handler if (listener.enabled) listener.StartListening(); } // Notify subscribers that pipeline is starting BeforeStartedPipeline?.Invoke(this); Log($"Started Pipeline with {TrackingObjects.Count} tracking target objects..."); AfterStartedPipeline?.Invoke(this); if (!RunningPipelines.Contains(this)) RunningPipelines.Add(this); _cleanupCalled = false; // Reset cleanup flag for this lifecycle } /// /// Unity OnDisable callback that triggers pipeline cleanup. /// Called when the GameObject becomes inactive in the scene. /// private void OnDisable() { Cleanup(); } /// /// Stops pipeline operation and cleans up all resources and event subscriptions. /// Ensures cleanup operations are performed only once per lifecycle. /// Disconnects events, stops endpoints, and resets state for potential restart. /// private void Cleanup() { // Prevent duplicate cleanup calls if (_cleanupCalled) return; Log("Cleaning up Pipeline..."); _cleanupCalled = true; BeforeStoppedPipeline?.Invoke(this); // Clear tracked objects TrackingObjects.Clear(); // Disconnect listener events and stop listening foreach (var listener in Listeners.Where(l => l.enabled)) { if (listener == null) continue; listener.OnFoundObjects -= FoundObjects; } // Check if this pipeline is the last one if (IsLastRunningPipeline) { // Trigger finally OnQuit event. if (_isQuittingApplication) { OnQuit?.Invoke(this); // todo: still not perfect!!! } // Stop all enabled endpoints from sending data foreach (var endpoint in DataProviders.SelectMany(dp => dp.Endpoints)) { if (endpoint == null || !endpoint.enabled) continue; endpoint.StopSending(); } } Log("Cleaned and stopped Pipeline."); AfterStoppedPipeline?.Invoke(this); if (IsLastRunningPipeline) { // Disconnect data provider events to prevent memory leaks foreach (var dp in DataProviders) { foreach (var c in dp.Composers) { c.AfterComposed -= AfterComposed; } foreach (var ep in dp.Endpoints) { ep.OnSendingStatement -= OnSendingStatement; ep.OnSentStatement -= OnSentStatement; } } } if (RunningPipelines.Contains(this)) RunningPipelines.Remove(this); _startupCalled = false; // Reset startup flag for potential restart } /// /// Manually starts the pipeline by activating the GameObject and calling startup logic. /// Provides programmatic control over pipeline activation. /// public void StartPipeline() { if (IsRunning) return; Log("Starting Pipeline..."); gameObject.SetActive(true); Startup(); } /// /// Unity OnApplicationQuit callback that ensures cleanup on application exit. /// Guarantees proper resource cleanup even if normal shutdown doesn't occur. /// protected virtual void OnAppQuit() { _isQuittingApplication = true; Cleanup(); } /// /// Unity OnDestroy callback that ensures cleanup when the GameObject is destroyed. /// Guarantees proper resource cleanup during scene changes or object destruction. /// private void OnDestroy() { Cleanup(); } /// /// Manually stops the pipeline by calling cleanup logic and deactivating the GameObject. /// Provides programmatic control over pipeline deactivation. /// public void StopPipeline() { if (!IsRunning) return; Log("Stopping Pipeline..."); Cleanup(); _startupCalled = false; gameObject.SetActive(false); } /// /// Enables or disables specific actions by name, affecting their event processing. /// Provides runtime control over which actions are tracked and processed. /// /// Whether to disable (true) or enable (false) the actions /// Collection of action names to affect, or null for all actions public void SetDisabledActions(bool disabled, IEnumerable names = null) { foreach (var pair in Actions.Where(p => names == null || names.Contains(p.Key))) { pair.Value.ForEach(v => v.IsDisabled = disabled); } } /// /// Enables or disables specific gestures by name, affecting their event processing. /// Provides runtime control over which gestures are tracked and processed. /// /// Whether to disable (true) or enable (false) the gestures /// Collection of gesture names to affect, or null for all gestures public void SetDisabledGestures(bool disabled, IEnumerable names = null) { foreach (var pair in Gestures.Where(p => names == null || names.Contains(p.Key))) { pair.Value.ForEach(v => v.IsDisabled = disabled); } } /// /// Event handler for objects found by listeners. /// Applies the filter chain to found objects and adds results to tracking collection. /// Core method that processes objects through the pipeline's filtering stages. /// /// Array of objects found by listeners private void FoundObjects(Object[] objects) { // Notify subscribers about found objects before filtering AfterFoundObjects?.Invoke(objects); // Apply all enabled filters in sequence to refine the object set objects = Filters.Where(f => f.enabled).Aggregate(objects, (gos, filter) => filter.Pass(gos)) ?? Array.Empty(); // Notify subscribers about filtered objects AfterFilteredObjects?.Invoke(objects); // Add filtered objects to the tracking collection TrackingObjects.AddRange(objects); } } }