1use crate::{actions::ActionContext, ActionError, NodeManifest, ParamUpdate};
38use std::collections::HashMap;
39
40pub type ActionId = u32;
42
43pub mod standard_actions {
45 use super::ActionId;
46 pub const RESET: ActionId = 0;
47 pub const RANDOMIZE: ActionId = 1;
48}
49
50#[derive(Debug, Clone)]
52pub enum ActionHandler {
53 Standard(StandardAction),
55 Custom(String),
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum StandardAction {
62 Reset,
63 Randomize,
64}
65
66pub struct ActionRegistry {
71 id_map: HashMap<String, ActionId>,
73
74 handlers: HashMap<ActionId, ActionHandler>,
76
77 manifest: NodeManifest,
79}
80
81impl ActionRegistry {
82 pub fn from_manifest(manifest: NodeManifest) -> Self {
92 let mut id_map = HashMap::new();
93 let mut handlers = HashMap::new();
94
95 id_map.insert("reset".to_string(), standard_actions::RESET);
98 handlers.insert(
99 standard_actions::RESET,
100 ActionHandler::Standard(StandardAction::Reset),
101 );
102
103 id_map.insert("randomize".to_string(), standard_actions::RANDOMIZE);
104 handlers.insert(
105 standard_actions::RANDOMIZE,
106 ActionHandler::Standard(StandardAction::Randomize),
107 );
108
109 let mut next_id = 100;
111 for action in &manifest.actions {
112 if !id_map.contains_key(&action.id) {
114 id_map.insert(action.id.clone(), next_id);
115 handlers.insert(next_id, ActionHandler::Custom(action.id.clone()));
116 next_id += 1;
117 }
118 }
119
120 Self {
121 id_map,
122 handlers,
123 manifest,
124 }
125 }
126
127 pub fn trigger(
140 &self,
141 name: &str,
142 ctx: &ActionContext,
143 seed: u64,
144 ) -> Result<Vec<ParamUpdate>, ActionError> {
145 let id = self
146 .id_map
147 .get(name)
148 .ok_or_else(|| ActionError::ActionNotFound(name.to_string()))?;
149
150 self.execute(*id, ctx, seed)
151 }
152
153 pub fn execute(
166 &self,
167 id: ActionId,
168 ctx: &ActionContext,
169 seed: u64,
170 ) -> Result<Vec<ParamUpdate>, ActionError> {
171 let handler = self
172 .handlers
173 .get(&id)
174 .ok_or_else(|| ActionError::ActionNotFound(id.to_string()))?;
175
176 match handler {
177 ActionHandler::Standard(action) => self.execute_standard(action, ctx, seed),
178 ActionHandler::Custom(name) => Err(ActionError::ExecutionFailed(format!(
179 "Custom action '{}' requires WASM execution (not available in registry)",
180 name
181 ))),
182 }
183 }
184
185 fn execute_standard(
189 &self,
190 action: &StandardAction,
191 ctx: &ActionContext,
192 seed: u64,
193 ) -> Result<Vec<ParamUpdate>, ActionError> {
194 match action {
195 StandardAction::Reset => crate::actions::calculate_reset_values(&self.manifest, ctx)
196 .map_err(ActionError::ExecutionFailed),
197
198 StandardAction::Randomize => {
199 crate::actions::calculate_random_values(&self.manifest, ctx, seed)
200 .map_err(ActionError::ExecutionFailed)
201 }
202 }
203 }
204
205 pub fn get_id(&self, name: &str) -> Option<ActionId> {
207 self.id_map.get(name).copied()
208 }
209
210 pub fn action_count(&self) -> usize {
212 self.handlers.len()
213 }
214
215 pub fn is_standard(&self, id: ActionId) -> bool {
217 id < 100
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::{
225 ActionConfig, ActionDef, ExecutionModel, NodeCategory, ParamType, ShaderParam,
226 SliderConfig, WidgetConfig,
227 };
228
229 fn create_test_manifest() -> NodeManifest {
230 NodeManifest {
231 api_version: 1,
232 display_name: "Test Node".to_string(),
233 version: "1.0.0".to_string(),
234 author: "Test".to_string(),
235 description: "Test node".to_string(),
236 category: NodeCategory::Effector,
237 tags: vec![],
238 model: ExecutionModel::FragmentShader,
239 parameters: vec![ShaderParam {
240 name: "strength".to_string(),
241 data_type: ParamType::ScalarF32,
242 widget: WidgetConfig::Slider(SliderConfig {
243 min: 0.0,
244 max: 1.0,
245 step: 0.01,
246 }),
247 }],
248 ports: vec![],
249 output_resolution_scale: 1.0,
250 output_hint: None,
251 actions: vec![
252 ActionDef {
253 id: "custom-action-1".to_string(),
254 label: "Custom Action 1".to_string(),
255 config: ActionConfig::Trigger,
256 },
257 ActionDef {
258 id: "custom-action-2".to_string(),
259 label: "Custom Action 2".to_string(),
260 config: ActionConfig::BeatSync,
261 },
262 ],
263 embedded_textures: vec![],
264 }
265 }
266
267 #[test]
268 fn test_registry_creation() {
269 let manifest = create_test_manifest();
270 let registry = ActionRegistry::from_manifest(manifest);
271
272 assert_eq!(registry.get_id("reset"), Some(standard_actions::RESET));
274 assert_eq!(
275 registry.get_id("randomize"),
276 Some(standard_actions::RANDOMIZE)
277 );
278
279 assert_eq!(registry.get_id("custom-action-1"), Some(100));
281 assert_eq!(registry.get_id("custom-action-2"), Some(101));
282
283 assert_eq!(registry.action_count(), 4);
285 }
286
287 #[test]
288 fn test_trigger_by_name() {
289 let manifest = create_test_manifest();
290 let registry = ActionRegistry::from_manifest(manifest);
291 let ctx = ActionContext {
292 params: &[],
293 beat_info: None,
294 is_high_frequency: false,
295 };
296
297 let result = registry.trigger("reset", &ctx, 0);
299 assert!(result.is_ok());
300 let updates = result.unwrap();
301 assert_eq!(updates.len(), 1); let result = registry.trigger("randomize", &ctx, 42);
305 assert!(result.is_ok());
306
307 let result = registry.trigger("unknown", &ctx, 0);
309 assert!(matches!(result, Err(ActionError::ActionNotFound(_))));
310 }
311
312 #[test]
313 fn test_execute_by_id() {
314 let manifest = create_test_manifest();
315 let registry = ActionRegistry::from_manifest(manifest);
316 let ctx = ActionContext {
317 params: &[],
318 beat_info: None,
319 is_high_frequency: false,
320 };
321
322 let result = registry.execute(standard_actions::RESET, &ctx, 0);
324 assert!(result.is_ok());
325
326 let result = registry.execute(standard_actions::RANDOMIZE, &ctx, 42);
328 assert!(result.is_ok());
329
330 let result = registry.execute(999, &ctx, 0);
332 assert!(matches!(result, Err(ActionError::ActionNotFound(_))));
333 }
334
335 #[test]
336 fn test_custom_action_fails_without_wasm() {
337 let manifest = create_test_manifest();
338 let registry = ActionRegistry::from_manifest(manifest);
339 let ctx = ActionContext {
340 params: &[],
341 beat_info: None,
342 is_high_frequency: false,
343 };
344
345 let result = registry.trigger("custom-action-1", &ctx, 0);
347 assert!(matches!(result, Err(ActionError::ExecutionFailed(_))));
348 }
349
350 #[test]
351 fn test_is_standard() {
352 let manifest = create_test_manifest();
353 let registry = ActionRegistry::from_manifest(manifest);
354
355 assert!(registry.is_standard(0)); assert!(registry.is_standard(1)); assert!(!registry.is_standard(100)); assert!(!registry.is_standard(101)); }
360}