mecha10_cli/services/
simulation.rs

1#![allow(dead_code)]
2
3//! Simulation service for Godot scene generation and management
4//!
5//! This service provides operations for generating and managing Godot simulations,
6//! including robot scene generation, environment selection, and validation.
7
8use crate::sim::{EnvironmentSelector, RobotGenerator, RobotProfile};
9use anyhow::{Context, Result};
10use std::path::{Path, PathBuf};
11use std::process::Command;
12
13/// Simulation service for managing Godot simulations
14///
15/// # Examples
16///
17/// ```rust,ignore
18/// use mecha10_cli::services::SimulationService;
19/// use std::path::Path;
20///
21/// # async fn example() -> anyhow::Result<()> {
22/// let service = SimulationService::new();
23///
24/// // Validate Godot installation
25/// service.validate_godot()?;
26///
27/// // Generate simulation
28/// service.generate(
29///     Path::new("mecha10.json"),
30///     Path::new("simulation/godot"),
31///     5, // max environments
32///     0  // min score
33/// )?;
34///
35/// // Run simulation
36/// service.run(
37///     "rover-robot",
38///     "warehouse",
39///     false // not headless
40/// )?;
41/// # Ok(())
42/// # }
43/// ```
44pub struct SimulationService {
45    /// Base path for simulation output
46    base_path: PathBuf,
47}
48
49impl SimulationService {
50    /// Create a new simulation service with default paths
51    pub fn new() -> Self {
52        Self {
53            base_path: PathBuf::from("simulation/godot"),
54        }
55    }
56
57    /// Create a simulation service with custom base path
58    ///
59    /// # Arguments
60    ///
61    /// * `base_path` - Base directory for simulation output
62    pub fn with_base_path(base_path: impl Into<PathBuf>) -> Self {
63        Self {
64            base_path: base_path.into(),
65        }
66    }
67
68    /// Validate Godot installation
69    ///
70    /// Checks if Godot 4.x is installed and accessible.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if Godot is not found or version is incorrect.
75    pub fn validate_godot(&self) -> Result<GodotInfo> {
76        // Try common Godot executable locations
77        let godot_paths = if cfg!(target_os = "macos") {
78            vec![
79                "/Applications/Godot.app/Contents/MacOS/Godot",
80                "/usr/local/bin/godot",
81                "/opt/homebrew/bin/godot",
82            ]
83        } else if cfg!(target_os = "linux") {
84            vec!["/usr/bin/godot", "/usr/local/bin/godot", "/snap/bin/godot"]
85        } else {
86            vec!["godot.exe", "C:\\Program Files\\Godot\\godot.exe"]
87        };
88
89        // First try 'godot' command from PATH
90        if let Ok(output) = Command::new("godot").arg("--version").output() {
91            if output.status.success() {
92                let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
93                return Ok(GodotInfo {
94                    path: "godot".to_string(),
95                    version,
96                    in_path: true,
97                });
98            }
99        }
100
101        // If not found in PATH, try specific locations
102        for path in &godot_paths {
103            if std::path::Path::new(path).exists() {
104                if let Ok(output) = Command::new(path).arg("--version").output() {
105                    if output.status.success() {
106                        let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
107                        return Ok(GodotInfo {
108                            path: path.to_string(),
109                            version,
110                            in_path: false,
111                        });
112                    }
113                }
114            }
115        }
116
117        Err(anyhow::anyhow!(
118            "Godot not found\n\n\
119            Godot 4.x is required to generate and run simulations.\n\n\
120            Installation instructions:\n\
121            • macOS: brew install godot or download from https://godotengine.org/download/macos\n\
122            • Linux: sudo apt install godot or download from https://godotengine.org/download/linux\n\
123            • Windows: Download from https://godotengine.org/download/windows\n\n\
124            After installation, ensure 'godot' is in your PATH or installed to a standard location."
125        ))
126    }
127
128    /// Generate simulation scenes from project configuration
129    ///
130    /// # Arguments
131    ///
132    /// * `config_path` - Path to mecha10.json
133    /// * `output_path` - Output directory for simulation files
134    /// * `max_envs` - Maximum number of environments to generate
135    /// * `min_score` - Minimum compatibility score (0-100)
136    pub fn generate(
137        &self,
138        config_path: &Path,
139        output_path: &Path,
140        max_envs: usize,
141        min_score: i32,
142    ) -> Result<GenerationResult> {
143        // Validate Godot first
144        let godot_info = self.validate_godot()?;
145
146        // Load robot profile
147        let profile = RobotProfile::from_config_file(config_path)?;
148
149        // Generate robot scene
150        let robot_output = output_path.join("robot.tscn");
151        let robot_generator = RobotGenerator::from_config_file(config_path)?;
152        robot_generator.generate(&robot_output)?;
153
154        // Select environments
155        let selector = EnvironmentSelector::new()?;
156        let matches = selector.select_environments(&profile, max_envs)?;
157
158        let filtered_matches: Vec<_> = matches.into_iter().filter(|m| m.score >= min_score).collect();
159
160        if filtered_matches.is_empty() {
161            return Err(anyhow::anyhow!(
162                "No matching environments found (min score: {})",
163                min_score
164            ));
165        }
166
167        // Count available environments
168        let _catalog_path = PathBuf::from("packages/simulation/environments/robot-tasks/catalog.json");
169        let base_path = PathBuf::from("packages/simulation/environments/robot-tasks");
170        let mut available_count = 0;
171
172        for env_match in &filtered_matches {
173            let env_path = base_path.join(&env_match.environment.path);
174            if env_path.exists() {
175                available_count += 1;
176            }
177        }
178
179        Ok(GenerationResult {
180            robot_scene: robot_output,
181            environments: filtered_matches.iter().map(|m| m.environment.id.clone()).collect(),
182            available_count,
183            godot_info,
184        })
185    }
186
187    /// Generate only the robot scene
188    ///
189    /// # Arguments
190    ///
191    /// * `config_path` - Path to mecha10.json
192    /// * `output_path` - Output path for robot.tscn
193    pub fn generate_robot(&self, config_path: &Path, output_path: &Path) -> Result<()> {
194        let robot_generator = RobotGenerator::from_config_file(config_path)?;
195        robot_generator.generate(output_path)
196    }
197
198    /// List available environments
199    ///
200    /// # Arguments
201    ///
202    /// * `config_path` - Path to mecha10.json
203    /// * `verbose` - Show detailed information
204    pub fn list_environments(&self, config_path: &Path, verbose: bool) -> Result<Vec<EnvironmentInfo>> {
205        let profile = RobotProfile::from_config_file(config_path)?;
206        let selector = EnvironmentSelector::new()?;
207        let matches = selector.select_environments(&profile, 100)?; // Get all
208
209        let base_path = PathBuf::from("packages/simulation/environments/robot-tasks");
210        let mut environments = Vec::new();
211
212        for env_match in matches {
213            let env_path = base_path.join(&env_match.environment.path);
214            let available = env_path.exists();
215
216            if verbose || available {
217                environments.push(EnvironmentInfo {
218                    id: env_match.environment.id.clone(),
219                    name: env_match.environment.name.clone(),
220                    description: env_match.environment.description.clone(),
221                    score: env_match.score,
222                    available,
223                });
224            }
225        }
226
227        Ok(environments)
228    }
229
230    /// Validate project configuration for simulation
231    ///
232    /// # Arguments
233    ///
234    /// * `config_path` - Path to mecha10.json
235    pub fn validate_config(&self, config_path: &Path) -> Result<ValidationResult> {
236        let profile = RobotProfile::from_config_file(config_path)?;
237
238        let mut errors = Vec::new();
239        let mut warnings = Vec::new();
240
241        // Validate platform
242        if profile.platform.is_empty() {
243            errors.push("Platform not specified".to_string());
244        }
245
246        // Validate sensors
247        if profile.sensors.is_empty() {
248            warnings.push("No sensors configured".to_string());
249        }
250
251        // Validate task nodes
252        if profile.task_nodes.is_empty() {
253            warnings.push("No task nodes configured".to_string());
254        }
255
256        let is_valid = errors.is_empty();
257
258        Ok(ValidationResult {
259            is_valid,
260            errors,
261            warnings,
262            platform: profile.platform,
263            sensor_count: profile.sensors.len(),
264            node_count: profile.task_nodes.len(),
265        })
266    }
267
268    /// Run a simulation in Godot
269    ///
270    /// # Arguments
271    ///
272    /// * `robot_name` - Name of the robot
273    /// * `env_id` - Environment ID
274    /// * `headless` - Run without UI
275    pub fn run(&self, robot_name: &str, env_id: &str, headless: bool) -> Result<()> {
276        // Validate Godot is available
277        let godot_info = self.validate_godot()?;
278
279        // Determine Godot project path
280        let godot_project_path = if let Ok(framework_path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
281            // Framework dev mode - use monorepo path
282            PathBuf::from(framework_path).join("packages/simulation/godot-project")
283        } else {
284            // Generated project mode - use relative path
285            PathBuf::from("simulation/godot")
286        };
287
288        let mut cmd = Command::new(&godot_info.path);
289        cmd.arg("--path").arg(godot_project_path);
290
291        if headless {
292            cmd.arg("--headless");
293        }
294
295        cmd.arg("--")
296            .arg(format!("--env={}", env_id))
297            .arg(format!("--robot={}", robot_name));
298
299        // Read config paths from mecha10.json if it exists
300        let mecha10_config_path = PathBuf::from("mecha10.json");
301        if mecha10_config_path.exists() {
302            if let Ok(config_content) = std::fs::read_to_string(&mecha10_config_path) {
303                if let Ok(config) = serde_json::from_str::<crate::types::ProjectConfig>(&config_content) {
304                    if let Some(sim_config) = config.simulation {
305                        // Pass model config path if specified
306                        if let Some(model_config) = sim_config.model_config {
307                            let model_config_path = PathBuf::from(&model_config);
308                            if model_config_path.exists() {
309                                cmd.arg(format!("--model-config={}", model_config_path.display()));
310                            }
311                        }
312
313                        // Pass environment config path if specified
314                        if let Some(env_config) = sim_config.environment_config {
315                            let env_config_path = PathBuf::from(&env_config);
316                            if env_config_path.exists() {
317                                cmd.arg(format!("--env-config={}", env_config_path.display()));
318                            }
319                        }
320                    }
321                }
322            }
323        } else {
324            // Framework dev mode or no mecha10.json - use path resolution
325            // Use the resolve functions to ensure proper priority (project > framework)
326            if let Ok(env_path) = self.resolve_environment_path(env_id) {
327                let env_config_path = env_path.join("environment.json");
328                if env_config_path.exists() {
329                    cmd.arg(format!("--env-config={}", env_config_path.display()));
330                }
331            }
332
333            if let Ok(model_path) = self.resolve_model_path(robot_name) {
334                let model_config_path = model_path.join("model.json");
335                if model_config_path.exists() {
336                    cmd.arg(format!("--model-config={}", model_config_path.display()));
337                }
338            }
339        }
340
341        let status = cmd.status().context("Failed to launch Godot")?;
342
343        if !status.success() {
344            return Err(anyhow::anyhow!("Godot exited with error code: {:?}", status.code()));
345        }
346
347        Ok(())
348    }
349
350    /// Check if simulation is properly set up
351    pub fn is_setup(&self) -> bool {
352        self.base_path.exists() && self.base_path.join("robot.tscn").exists()
353    }
354
355    /// Get the base path for simulations
356    pub fn base_path(&self) -> &Path {
357        &self.base_path
358    }
359
360    /// Resolve a model path from package namespace or plain name
361    ///
362    /// Supports multiple formats:
363    /// - Package namespace: "@mecha10/simulation-models/rover"
364    /// - Plain name: "rover"
365    ///
366    /// Resolution order:
367    /// 1. Framework (monorepo): packages/simulation/models/{name}/
368    /// 2. Project-local: simulation/models/{name}/
369    ///
370    /// # Arguments
371    ///
372    /// * `model_ref` - Model reference (package namespace or plain name)
373    ///
374    /// # Returns
375    ///
376    /// Absolute path to the model directory, or error if not found
377    pub fn resolve_model_path(&self, model_ref: &str) -> Result<PathBuf> {
378        // Strip package namespace if present
379        let model_name = model_ref
380            .strip_prefix("@mecha10/simulation-models/")
381            .unwrap_or(model_ref);
382
383        // Try project-local path FIRST (project customizations take precedence)
384        let project_model_path = PathBuf::from("simulation/models").join(model_name);
385        if project_model_path.exists() {
386            return Ok(project_model_path);
387        }
388
389        // Fall back to framework path (monorepo)
390        if let Ok(framework_path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
391            let framework_model_path = PathBuf::from(framework_path)
392                .join("packages/simulation/models")
393                .join(model_name);
394            if framework_model_path.exists() {
395                return Ok(framework_model_path);
396            }
397        }
398
399        Err(anyhow::anyhow!(
400            "Model not found: {}\n\nChecked:\n  • Project: simulation/models/{}\n  • Framework: packages/simulation/models/{}",
401            model_ref,
402            model_name,
403            model_name
404        ))
405    }
406
407    /// Resolve an environment path from package namespace or plain name
408    ///
409    /// Supports multiple formats:
410    /// - Package namespace: "@mecha10/simulation-environments/basic_arena"
411    /// - Plain name: "basic_arena"
412    ///
413    /// Resolution order:
414    /// 1. Project-local: simulation/environments/{name}/ (project customizations take precedence)
415    /// 2. Framework (monorepo): packages/simulation/environments/{name}/
416    ///
417    /// # Arguments
418    ///
419    /// * `env_ref` - Environment reference (package namespace or plain name)
420    ///
421    /// # Returns
422    ///
423    /// Absolute path to the environment directory, or error if not found
424    pub fn resolve_environment_path(&self, env_ref: &str) -> Result<PathBuf> {
425        // Strip package namespace if present
426        let env_name = env_ref
427            .strip_prefix("@mecha10/simulation-environments/")
428            .unwrap_or(env_ref);
429
430        // Try project-local path FIRST (project customizations take precedence)
431        let project_env_path = PathBuf::from("simulation/environments").join(env_name);
432        if project_env_path.exists() {
433            return Ok(project_env_path);
434        }
435
436        // Fall back to framework path (monorepo)
437        if let Ok(framework_path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
438            let framework_env_path = PathBuf::from(framework_path)
439                .join("packages/simulation/environments")
440                .join(env_name);
441            if framework_env_path.exists() {
442                return Ok(framework_env_path);
443            }
444        }
445
446        Err(anyhow::anyhow!(
447            "Environment not found: {}\n\nChecked:\n  • Project: simulation/environments/{}\n  • Framework: packages/simulation/environments/{}",
448            env_ref,
449            env_name,
450            env_name
451        ))
452    }
453}
454
455impl Default for SimulationService {
456    fn default() -> Self {
457        Self::new()
458    }
459}
460
461/// Information about Godot installation
462#[derive(Debug, Clone)]
463pub struct GodotInfo {
464    pub path: String,
465    pub version: String,
466    pub in_path: bool,
467}
468
469/// Result of simulation generation
470#[derive(Debug)]
471pub struct GenerationResult {
472    pub robot_scene: PathBuf,
473    pub environments: Vec<String>,
474    pub available_count: usize,
475    pub godot_info: GodotInfo,
476}
477
478/// Information about an environment
479#[derive(Debug, Clone)]
480pub struct EnvironmentInfo {
481    pub id: String,
482    pub name: String,
483    pub description: String,
484    pub score: i32,
485    pub available: bool,
486}
487
488/// Result of configuration validation
489#[derive(Debug)]
490pub struct ValidationResult {
491    pub is_valid: bool,
492    pub errors: Vec<String>,
493    pub warnings: Vec<String>,
494    pub platform: String,
495    pub sensor_count: usize,
496    pub node_count: usize,
497}