/* * 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.Schedules; using UnityEngine; using Object = UnityEngine.Object; namespace OmiLAXR.TrackingBehaviours { /// /// Non-generic base class for tracking behaviors that work with general Unity Objects. /// Provides a simplified API by using Object as the tracking type, eliminating the need /// for generic type parameters when working with mixed object types. /// public abstract class TrackingBehaviour : TrackingBehaviour { /// /// Empty implementation to be overridden by derived classes if needed. /// Called after objects have been filtered through the pipeline. Can be called multiple times. /// Base implementation intentionally does nothing - derived classes override to process objects. /// /// Array of filtered objects to track protected override void AfterFilteredObjects(Object[] objects) { // Base implementation is empty - derived classes should override if needed } } /// /// Generic base class for all tracking behaviors in the OmiLAXR system. /// Provides functionality to track specific types of objects through the pipeline. /// Executes early in Unity's order to ensure tracking is set up before other components. /// /// This class handles the complete lifecycle of tracking behaviors: /// - Pipeline event subscription and cleanup /// - Object filtering and type-safe processing /// - Scheduler management for timed operations /// - Event binding and unbinding for memory management /// /// Type of Unity Objects to track (must inherit from UnityEngine.Object) [DefaultExecutionOrder(-1)] public abstract class TrackingBehaviour : ActorPipelineComponent, ITrackingBehaviour where T : Object { /// /// Collection of schedulers that control when this tracking behavior executes. /// Manages interval timers, timeout operations, and other scheduled tasks. /// All schedulers are automatically started/stopped with the pipeline lifecycle. /// protected readonly List Schedulers = new List(); /// /// Creates a simple interval scheduler with just the tick action and interval duration. /// Convenience method for basic interval operations without advanced configuration. /// /// Action to execute at each interval /// Time between executions in seconds /// Configured IntervalTicker instance protected IntervalTicker SetInterval(Action onTick, float intervalSeconds = 1.0f, int ttLTicks = -1) => SetInterval(IntervalTicker.Create(this, intervalSeconds, onTick, ttLTicks, runImmediate: true)); protected IntervalTicker SetInterval(IntervalTicker ticker) => (IntervalTicker)SetTicker(ticker); protected Scheduler SetTicker(Scheduler ticker) { Schedulers.Add(ticker); return ticker; } /// /// Creates and configures a timeout-based scheduler with full settings control. /// The scheduler will execute the action once after the specified timeout period. /// /// Action to execute when timeout expires /// Detailed configuration for the timeout timer /// Optional action called before timeout starts /// Optional action called after timeout completes /// Configured TimeoutTicker instance for further control protected TimeoutTicker SetTimeout(TimeoutTicker timeoutTicker) => (TimeoutTicker)SetTicker(timeoutTicker); /// /// Creates a simple timeout scheduler with just the tick action and timeout duration. /// Convenience method for basic timeout operations without advanced configuration. /// /// Action to execute when timeout expires /// Timeout duration in seconds /// Configured TimeoutTicker instance protected TimeoutTicker SetTimeout(Action onTick, float timeoutSeconds = 1.0f) => SetTimeout(TimeoutTicker.Create(this, timeoutSeconds, onTick, runImmediate: true)); /// /// Initializes the tracking behavior by setting up schedulers and event subscriptions. /// Connects to pipeline events to receive objects for tracking and manages the complete /// lifecycle of the tracking behavior from pipeline start to stop. /// /// This method is critical for proper tracking behavior operation and handles: /// - Pipeline lifecycle event binding /// - Object discovery and filtering event handling /// - Error handling for initialization failures /// - Scheduler lifecycle management /// protected override void OnEnable() { try { // Set up pipeline lifecycle event handlers Pipeline.BeforeStartedPipeline += _ => { if (!enabled) return; BeforeStartedPipeline(Pipeline); }; // Handle pipeline start - this is where tracking begins Pipeline.AfterStartedPipeline += _ => { if (!enabled) return; OnStartedPipeline(Pipeline); }; // Handle pipeline preparation for stop Pipeline.BeforeStoppedPipeline += _ => { if (!enabled) return; BeforeStoppedPipeline(Pipeline); }; // Handle pipeline stop - cleanup and resource disposal Pipeline.AfterStoppedPipeline += _ => { if (!enabled) return; OnStoppedPipeline(Pipeline); ClearSchedules(); // Stop and clean up all schedulers Dispose(AllFilteredObjects.Select(o => o as Object).ToArray()); // Clean up tracked objects }; // Handle objects discovered by the pipeline (before filtering) Pipeline.AfterFoundObjects += (objects) => { if (!enabled) return; // Type optimization: skip selection if T is already Object type AfterFoundObjects(typeof(T) == typeof(Object) ? objects as T[] : Select(objects)); }; // Handle objects after they've been filtered by the pipeline (main tracking target) Pipeline.AfterFilteredObjects += (objects) => { if (!enabled) return; var selectedObjects = ExtractOrSelect(objects); var newSelectedObjects = new List(); // Filter objects to match our tracking type and store them foreach (var o in objects) { if (AllFilteredObjects.Contains(o)) continue; AllFilteredObjects.Add(o); } foreach (var s in selectedObjects) { if (SelectedObjects.Contains(s)) continue; SelectedObjects.Add(s); newSelectedObjects.Add(s); } AfterFilteredObjects(newSelectedObjects.ToArray()); }; Pipeline.OnQuit += (_) => { if (!enabled) return; OnQuitPipeline(Pipeline); }; } catch (Exception ex) { // Log initialization errors but don't crash the application DebugLog.OmiLAXR.Error($"Error initializing {GetType().Name}: {ex.Message}"); } } /// /// Called when the pipeline is about to quit (Application quit). /// Override to perform cleanup operations before the pipeline stops. /// protected virtual void OnQuitPipeline(Pipeline pipeline) {} /// /// Called immediately before the pipeline starts. /// Override to perform pre-start initialization or validation. /// /// Reference to the pipeline about to start protected virtual void BeforeStartedPipeline(Pipeline pipeline) {} /// /// Called immediately before the pipeline stops. /// Override to perform cleanup preparation or save state before shutdown. /// /// Reference to the pipeline about to stop protected virtual void BeforeStoppedPipeline(Pipeline pipeline) {} /// /// Called when the pipeline starts. Override to implement custom start behavior. /// This is where tracking behaviors should initialize their tracking logic, /// set up event bindings, and prepare for object monitoring. /// /// Reference to the started pipeline protected virtual void OnStartedPipeline(Pipeline pipeline) {} /// /// Called before the pipeline stops. Override to implement custom stop behavior. /// Use this for final cleanup, data persistence, or graceful shutdown procedures. /// /// Reference to the stopping pipeline protected virtual void OnStoppedPipeline(Pipeline pipeline) {} /// /// Called after objects are found by the pipeline but before filtering. /// Override to implement custom processing on found objects, such as /// preliminary validation, logging, or preparation for filtering. /// /// Array of found objects of type T protected virtual void AfterFoundObjects(T[] objects) {} /// /// Called after objects have been filtered by the pipeline. /// Must be implemented by derived classes to process the filtered objects. /// Can be called multiple times as new objects are discovered or existing ones are updated. /// /// This is the main entry point for tracking logic - derived classes should: /// - Set up event bindings for the provided objects /// - Initialize tracking state for new objects /// - Configure monitoring parameters /// /// Array of filtered objects of type T to begin tracking protected abstract void AfterFilteredObjects(T[] objects); protected readonly List SelectedObjects = new List(); /// /// Stores the currently selected objects for tracking. /// Maintains a persistent list of all objects that have been filtered /// through the pipeline and are actively being tracked. /// Used for cleanup operations and state management. /// protected static readonly List AllFilteredObjects = new List(); /// /// Starts all registered schedulers if the component is enabled. /// Called automatically when the pipeline starts to begin timed operations. /// protected void StartSchedules() { if (!enabled) return; foreach (var scheduler in Schedulers) { if (!scheduler.owner) scheduler.owner = this; scheduler.Start(); } } /// /// Stops all registered schedulers without removing them from the collection. /// Preserves scheduler configuration for potential restart. /// protected void StopSchedules() { foreach (var scheduler in Schedulers) scheduler.Stop(); } /// /// Stops and removes all schedulers from the collection. /// Called during pipeline shutdown to ensure complete cleanup. /// After this call, new schedulers must be created for future operations. /// protected void ClearSchedules() { StopSchedules(); // Stop all running schedulers first Schedulers.Clear(); // Remove all scheduler references } /// /// Unity lifecycle method called when the component is disabled. /// Ensures proper cleanup of schedulers and tracking events to prevent /// memory leaks and dangling event references. /// protected virtual void OnDisable() { StopSchedules(); // Stop all running operations DisposeAllTrackingEvents(); // Clean up event bindings } /// /// Unbinds all tracking events to prevent memory leaks and unexpected behavior. /// Uses reflection to find all ITrackingBehaviourEvent fields and calls UnbindAll on each. /// This ensures complete cleanup of event subscriptions regardless of the specific event types used. /// protected void DisposeAllTrackingEvents() { // Get all fields that implement ITrackingBehaviourEvent interface var fields = GetTrackingBehaviourEvents(); foreach (var field in fields) { // Get the actual event instance from this tracking behavior var fieldValue = field.GetValue(this) as ITrackingBehaviourEvent; // Unbind all listeners to prevent memory leaks fieldValue?.UnbindAll(); } } /// /// Cleans up resources when tracking stops. /// Called automatically during pipeline shutdown with all tracked objects. /// Base implementation unbinds all tracking events - override to add custom cleanup. /// /// Array of objects that were being tracked protected virtual void Dispose(Object[] objects) { DisposeAllTrackingEvents(); // Clean up all event bindings by default } /// /// Retrieves all fields in this class that implement ITrackingBehaviourEvent. /// Used for automatic event management and cleanup operations. /// Searches both public and private instance fields to ensure complete coverage. /// /// Array of FieldInfo for fields of type ITrackingBehaviourEvent public FieldInfo[] GetTrackingBehaviourEvents() { return GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) .Where(f => typeof(ITrackingBehaviourEvent).IsAssignableFrom(f.FieldType)) .ToArray(); } /// /// Filters an array of Unity Objects to only include those of the specified type. /// Uses both exact type matching and inheritance checking to capture all compatible objects. /// Essential for type-safe object tracking in the generic pipeline system. /// /// Type to filter for (must inherit from UnityEngine.Object) /// Array of Unity Objects to filter /// Array of objects of type TS, with null objects filtered out protected TS[] Select(Object[] objects) where TS : Object => objects .Where(o => o.GetType() == typeof(TS) || o.GetType().IsSubclassOf(typeof(TS))) .Select(o => o as TS).ToArray(); protected TS[] Extract(Object[] objects) where TS : Component { return objects .Select(obj => { if (obj is GameObject go) return go.GetComponent(); if (obj is Component comp) return comp.GetComponent(); return null; }) .Where(c => c != null) .ToArray(); } protected TS[] ExtractOrSelect(Object[] objects) where TS : Object { var targetType = typeof(TS); // If TS is a Component or a subclass of Component, extract via GetComponent if (typeof(Component).IsAssignableFrom(targetType)) { return objects .Select(obj => { if (obj is GameObject go) return go.GetComponent(); if (obj is Component comp) return comp.GetComponent(); return null; }) .Where(c => c != null) .ToArray(); } // Otherwise, just filter and cast directly return objects .Where(o => targetType.IsAssignableFrom(o.GetType())) .Cast() .ToArray(); } /// /// Gets the first object of the specified type from an array of Unity Objects. /// Useful for finding singleton components or primary objects of a specific type. /// Returns null if no matching object is found. /// /// Type to find (must inherit from UnityEngine.Object) /// Array of Unity Objects to search /// The first object of type TS, or null if none exists protected TS First(Object[] objects) where TS : Object => (TS)objects .FirstOrDefault(o => o.GetType() == typeof(TS) || o.GetType().IsSubclassOf(typeof(TS))); } }