Skip to main content

khora_control/
registry.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Agent registry for automatic registration and ordered iteration.
16
17use khora_core::agent::dependency::AgentDependency;
18use khora_core::agent::timing::AgentImportance;
19use khora_core::agent::{Agent, EngineMode, ExecutionPhase};
20use khora_core::control::gorna::AgentId;
21use std::sync::{Arc, Mutex};
22
23/// Tuple returned by [`AgentRegistry::collect_for_phase`] for each agent
24/// matching the requested phase and mode: `(agent, importance, priority,
25/// dependencies)`.
26pub type PhaseAgentEntry = (
27    Arc<Mutex<dyn Agent>>,
28    AgentImportance,
29    f32,
30    Vec<AgentDependency>,
31);
32
33/// Entry in the agent registry containing the agent and its priority.
34struct AgentEntry {
35    agent: Arc<Mutex<dyn Agent>>,
36    priority: f32,
37    /// Modes in which this agent is active.
38    /// Empty = active in all modes.
39    active_modes: Vec<EngineMode>,
40}
41
42/// Registry that manages all registered agents with automatic priority ordering.
43///
44/// Agents are automatically sorted by priority (highest first) and can be
45/// iterated in execution order.
46pub struct AgentRegistry {
47    entries: Vec<AgentEntry>,
48}
49
50impl AgentRegistry {
51    /// Creates a new empty registry.
52    pub fn new() -> Self {
53        Self {
54            entries: Vec::new(),
55        }
56    }
57
58    /// Registers an agent with the given priority.
59    ///
60    /// Higher priority values mean the agent is updated first.
61    /// The agent is active in all engine modes.
62    pub fn register(&mut self, agent: Arc<Mutex<dyn Agent>>, priority: f32) {
63        self.register_for_mode(agent, priority, vec![]);
64    }
65
66    /// Registers an agent with the given priority, active only in the specified modes.
67    ///
68    /// Higher priority values mean the agent is updated first.
69    /// If `modes` is empty, the agent is active in all modes.
70    pub fn register_for_mode(
71        &mut self,
72        agent: Arc<Mutex<dyn Agent>>,
73        priority: f32,
74        modes: Vec<EngineMode>,
75    ) {
76        let id = agent.lock().map(|a| a.id()).unwrap_or(AgentId::Asset);
77        log::info!(
78            "AgentRegistry: Registered {:?} (priority={:.2}, modes={:?})",
79            id,
80            priority,
81            modes
82        );
83
84        self.entries.push(AgentEntry {
85            agent,
86            priority,
87            active_modes: modes,
88        });
89        self.entries.sort_by(|a, b| {
90            b.priority
91                .partial_cmp(&a.priority)
92                .unwrap_or(std::cmp::Ordering::Equal)
93        });
94    }
95
96    /// Returns the number of registered agents.
97    pub fn len(&self) -> usize {
98        self.entries.len()
99    }
100
101    /// Returns true if no agents are registered.
102    pub fn is_empty(&self) -> bool {
103        self.entries.is_empty()
104    }
105
106    /// Returns an iterator over all agents in priority order (highest first).
107    pub fn iter(&self) -> impl Iterator<Item = &Arc<Mutex<dyn Agent>>> {
108        self.entries.iter().map(|e| &e.agent)
109    }
110
111    /// Returns the [`AgentId`]s of every registered agent in priority order.
112    ///
113    /// Used by the scheduler to seed a fresh `AgentCompletionMap` per frame.
114    pub fn all_ids(&self) -> Vec<AgentId> {
115        self.entries
116            .iter()
117            .filter_map(|entry| entry.agent.lock().ok().map(|a| a.id()))
118            .collect()
119    }
120
121    /// Initializes all agents once after registration.
122    ///
123    /// Calls `on_initialize()` on each agent in priority order, giving them
124    /// access to the engine services for caching and lane setup.
125    pub fn initialize_all(&self, context: &mut khora_core::EngineContext<'_>) {
126        for entry in &self.entries {
127            if let Ok(mut agent) = entry.agent.lock() {
128                let id = agent.id();
129                agent.on_initialize(context);
130                log::info!("AgentRegistry: Initialized {:?}", id);
131            }
132        }
133    }
134
135    /// Executes all agents in priority order.
136    ///
137    /// Called each frame. Each agent selects the appropriate lanes and
138    /// dispatches their execution.
139    pub fn execute_all(&self, context: &mut khora_core::EngineContext<'_>) {
140        for entry in &self.entries {
141            if let Ok(mut agent) = entry.agent.lock() {
142                agent.execute(context);
143            }
144        }
145    }
146
147    /// Returns the agent with the given ID, if registered.
148    pub fn get_by_id(&self, id: AgentId) -> Option<Arc<Mutex<dyn Agent>>> {
149        for entry in &self.entries {
150            if let Ok(agent) = entry.agent.lock() {
151                if agent.id() == id {
152                    return Some(entry.agent.clone());
153                }
154            }
155        }
156        None
157    }
158
159    /// Collects agents that are allowed to run in the given phase and mode.
160    /// Returns a list of (agent, importance, priority, dependencies).
161    pub fn collect_for_phase(
162        &self,
163        phase: ExecutionPhase,
164        mode: &EngineMode,
165    ) -> Vec<PhaseAgentEntry> {
166        self.entries
167            .iter()
168            .filter_map(|entry| {
169                let agent = entry.agent.lock().ok()?;
170                let timing = agent.execution_timing();
171
172                // Filter by phase
173                if !timing.allowed_phases.contains(&phase) {
174                    return None;
175                }
176
177                // Filter by mode (empty = all modes)
178                if !entry.active_modes.is_empty() && !entry.active_modes.contains(mode) {
179                    return None;
180                }
181
182                Some((
183                    entry.agent.clone(),
184                    timing.importance,
185                    timing.priority,
186                    timing.dependencies,
187                ))
188            })
189            .collect()
190    }
191
192    /// Executes a specific agent by ID.
193    pub fn execute_agent(&self, id: AgentId, context: &mut khora_core::EngineContext<'_>) -> bool {
194        if let Some(agent) = self.get_by_id(id) {
195            if let Ok(mut a) = agent.lock() {
196                a.execute(context);
197                return true;
198            }
199        }
200        false
201    }
202}
203
204impl Default for AgentRegistry {
205    fn default() -> Self {
206        Self::new()
207    }
208}