1#![allow(dead_code)]
2
3use crate::paths;
9use crate::sim::{EnvironmentSelector, RobotGenerator, RobotProfile};
10use anyhow::{Context, Result};
11use std::path::{Path, PathBuf};
12use std::process::Command;
13
14pub struct SimulationService {
46 base_path: PathBuf,
48}
49
50impl SimulationService {
51 pub fn new() -> Self {
53 Self {
54 base_path: PathBuf::from(paths::project::SIMULATION_GODOT_DIR),
55 }
56 }
57
58 pub fn with_base_path(base_path: impl Into<PathBuf>) -> Self {
64 Self {
65 base_path: base_path.into(),
66 }
67 }
68
69 pub fn validate_godot(&self) -> Result<GodotInfo> {
77 let godot_paths = if cfg!(target_os = "macos") {
79 vec![
80 "/Applications/Godot.app/Contents/MacOS/Godot",
81 "/usr/local/bin/godot",
82 "/opt/homebrew/bin/godot",
83 ]
84 } else if cfg!(target_os = "linux") {
85 vec!["/usr/bin/godot", "/usr/local/bin/godot", "/snap/bin/godot"]
86 } else {
87 vec!["godot.exe", "C:\\Program Files\\Godot\\godot.exe"]
88 };
89
90 if let Ok(output) = Command::new("godot").arg("--version").output() {
92 if output.status.success() {
93 let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
94 return Ok(GodotInfo {
95 path: "godot".to_string(),
96 version,
97 in_path: true,
98 });
99 }
100 }
101
102 for path in &godot_paths {
104 if std::path::Path::new(path).exists() {
105 if let Ok(output) = Command::new(path).arg("--version").output() {
106 if output.status.success() {
107 let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
108 return Ok(GodotInfo {
109 path: path.to_string(),
110 version,
111 in_path: false,
112 });
113 }
114 }
115 }
116 }
117
118 Err(anyhow::anyhow!(
119 "Godot not found\n\n\
120 Godot 4.x is required to generate and run simulations.\n\n\
121 Installation instructions:\n\
122 • macOS: brew install godot or download from https://godotengine.org/download/macos\n\
123 • Linux: sudo apt install godot or download from https://godotengine.org/download/linux\n\
124 • Windows: Download from https://godotengine.org/download/windows\n\n\
125 After installation, ensure 'godot' is in your PATH or installed to a standard location."
126 ))
127 }
128
129 pub fn generate(
138 &self,
139 config_path: &Path,
140 output_path: &Path,
141 max_envs: usize,
142 min_score: i32,
143 ) -> Result<GenerationResult> {
144 let godot_info = self.validate_godot()?;
146
147 let profile = RobotProfile::from_config_file(config_path)?;
149
150 let robot_output = output_path.join("robot.tscn");
152 let robot_generator = RobotGenerator::from_config_file(config_path)?;
153 robot_generator.generate(&robot_output)?;
154
155 let selector = EnvironmentSelector::new()?;
157 let matches = selector.select_environments(&profile, max_envs)?;
158
159 let filtered_matches: Vec<_> = matches.into_iter().filter(|m| m.score >= min_score).collect();
160
161 if filtered_matches.is_empty() {
162 return Err(anyhow::anyhow!(
163 "No matching environments found (min score: {})",
164 min_score
165 ));
166 }
167
168 let _catalog_path = PathBuf::from(paths::framework::ROBOT_TASKS_CATALOG);
170 let base_path = PathBuf::from(paths::framework::ROBOT_TASKS_DIR);
171 let mut available_count = 0;
172
173 for env_match in &filtered_matches {
174 let env_path = base_path.join(&env_match.environment.path);
175 if env_path.exists() {
176 available_count += 1;
177 }
178 }
179
180 Ok(GenerationResult {
181 robot_scene: robot_output,
182 environments: filtered_matches.iter().map(|m| m.environment.id.clone()).collect(),
183 available_count,
184 godot_info,
185 })
186 }
187
188 pub fn generate_robot(&self, config_path: &Path, output_path: &Path) -> Result<()> {
195 let robot_generator = RobotGenerator::from_config_file(config_path)?;
196 robot_generator.generate(output_path)
197 }
198
199 pub fn list_environments(&self, config_path: &Path, verbose: bool) -> Result<Vec<EnvironmentInfo>> {
206 let profile = RobotProfile::from_config_file(config_path)?;
207 let selector = EnvironmentSelector::new()?;
208 let matches = selector.select_environments(&profile, 100)?; let base_path = PathBuf::from(paths::framework::ROBOT_TASKS_DIR);
211 let mut environments = Vec::new();
212
213 for env_match in matches {
214 let env_path = base_path.join(&env_match.environment.path);
215 let available = env_path.exists();
216
217 if verbose || available {
218 environments.push(EnvironmentInfo {
219 id: env_match.environment.id.clone(),
220 name: env_match.environment.name.clone(),
221 description: env_match.environment.description.clone(),
222 score: env_match.score,
223 available,
224 });
225 }
226 }
227
228 Ok(environments)
229 }
230
231 pub fn validate_config(&self, config_path: &Path) -> Result<ValidationResult> {
237 let profile = RobotProfile::from_config_file(config_path)?;
238
239 let mut errors = Vec::new();
240 let mut warnings = Vec::new();
241
242 if profile.platform.is_empty() {
244 errors.push("Platform not specified".to_string());
245 }
246
247 if profile.sensors.is_empty() {
249 warnings.push("No sensors configured".to_string());
250 }
251
252 if profile.task_nodes.is_empty() {
254 warnings.push("No task nodes configured".to_string());
255 }
256
257 let is_valid = errors.is_empty();
258
259 Ok(ValidationResult {
260 is_valid,
261 errors,
262 warnings,
263 platform: profile.platform,
264 sensor_count: profile.sensors.len(),
265 node_count: profile.task_nodes.len(),
266 })
267 }
268
269 pub fn run(&self, robot_name: &str, env_id: &str, headless: bool) -> Result<()> {
277 let godot_info = self.validate_godot()?;
279
280 let godot_project_path = if let Ok(framework_path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
282 PathBuf::from(framework_path).join(paths::framework::SIMULATION_GODOT_DIR)
284 } else {
285 PathBuf::from(paths::project::SIMULATION_GODOT_DIR)
287 };
288
289 let mut cmd = Command::new(&godot_info.path);
290 cmd.arg("--path").arg(godot_project_path);
291
292 if headless {
293 cmd.arg("--headless");
294 }
295
296 cmd.arg("--")
297 .arg(format!("--env={}", env_id))
298 .arg(format!("--robot={}", robot_name));
299
300 let mecha10_config_path = PathBuf::from(paths::PROJECT_CONFIG);
302 if mecha10_config_path.exists() {
303 if let Ok(config_content) = std::fs::read_to_string(&mecha10_config_path) {
304 if let Ok(config) = serde_json::from_str::<crate::types::ProjectConfig>(&config_content) {
305 if let Some(sim_config) = config.simulation {
306 if let Some(model_config) = sim_config.model_config {
308 let model_config_path = PathBuf::from(&model_config);
309 if model_config_path.exists() {
310 cmd.arg(format!("--model-config={}", model_config_path.display()));
311 }
312 }
313
314 if let Some(env_config) = sim_config.environment_config {
316 let env_config_path = PathBuf::from(&env_config);
317 if env_config_path.exists() {
318 cmd.arg(format!("--env-config={}", env_config_path.display()));
319 }
320 }
321 }
322 }
323 }
324 } else {
325 if let Ok(env_path) = self.resolve_environment_path(env_id) {
328 let env_config_path = env_path.join("environment.json");
329 if env_config_path.exists() {
330 cmd.arg(format!("--env-config={}", env_config_path.display()));
331 }
332 }
333
334 if let Ok(model_path) = self.resolve_model_path(robot_name) {
335 let model_config_path = model_path.join("model.json");
336 if model_config_path.exists() {
337 cmd.arg(format!("--model-config={}", model_config_path.display()));
338 }
339 }
340 }
341
342 let status = cmd.status().context("Failed to launch Godot")?;
343
344 if !status.success() {
345 return Err(anyhow::anyhow!("Godot exited with error code: {:?}", status.code()));
346 }
347
348 Ok(())
349 }
350
351 pub fn is_setup(&self) -> bool {
353 self.base_path.exists() && self.base_path.join("robot.tscn").exists()
354 }
355
356 pub fn base_path(&self) -> &Path {
358 &self.base_path
359 }
360
361 pub fn resolve_model_path(&self, model_ref: &str) -> Result<PathBuf> {
379 let model_name = model_ref
381 .strip_prefix("@mecha10/simulation-models/")
382 .unwrap_or(model_ref);
383
384 let project_model_path = PathBuf::from(paths::project::SIMULATION_MODELS_DIR).join(model_name);
386 if project_model_path.exists() {
387 return Ok(project_model_path);
388 }
389
390 if let Ok(framework_path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
392 let framework_model_path = PathBuf::from(framework_path)
393 .join(paths::framework::SIMULATION_MODELS_DIR)
394 .join(model_name);
395 if framework_model_path.exists() {
396 return Ok(framework_model_path);
397 }
398 }
399
400 let cli_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
402 if let Some(packages_dir) = cli_manifest_dir.parent() {
403 let bundled_model_path = packages_dir.join("simulation/models").join(model_name);
404 if bundled_model_path.exists() {
405 return Ok(bundled_model_path);
406 }
407 }
408
409 let assets_service = crate::services::SimulationAssetsService::new();
411 if let Some(models_path) = assets_service.models_path() {
412 let cached_model_path = models_path.join(model_name);
413 if cached_model_path.exists() {
414 return Ok(cached_model_path);
415 }
416 }
417
418 Err(anyhow::anyhow!(
419 "Model not found: {}\n\nChecked:\n • Project: simulation/models/{}\n • Framework: packages/simulation/models/{}\n • Cached: ~/.mecha10/simulation/current/models/{}",
420 model_ref,
421 model_name,
422 model_name,
423 model_name
424 ))
425 }
426
427 pub fn resolve_environment_path(&self, env_ref: &str) -> Result<PathBuf> {
445 let env_name = env_ref
447 .strip_prefix("@mecha10/simulation-environments/")
448 .unwrap_or(env_ref);
449
450 let project_env_path = PathBuf::from(paths::project::SIMULATION_ENVIRONMENTS_DIR).join(env_name);
452 if project_env_path.exists() {
453 return Ok(project_env_path);
454 }
455
456 if let Ok(framework_path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
458 let framework_env_path = PathBuf::from(framework_path)
459 .join(paths::framework::SIMULATION_ENVIRONMENTS_DIR)
460 .join(env_name);
461 if framework_env_path.exists() {
462 return Ok(framework_env_path);
463 }
464 }
465
466 let cli_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
468 if let Some(packages_dir) = cli_manifest_dir.parent() {
469 let bundled_env_path = packages_dir.join("simulation/environments").join(env_name);
470 if bundled_env_path.exists() {
471 return Ok(bundled_env_path);
472 }
473 }
474
475 let assets_service = crate::services::SimulationAssetsService::new();
477 if let Some(envs_path) = assets_service.environments_path() {
478 let cached_env_path = envs_path.join(env_name);
479 if cached_env_path.exists() {
480 return Ok(cached_env_path);
481 }
482 }
483
484 Err(anyhow::anyhow!(
485 "Environment not found: {}\n\nChecked:\n • Project: simulation/environments/{}\n • Framework: packages/simulation/environments/{}\n • Cached: ~/.mecha10/simulation/current/environments/{}",
486 env_ref,
487 env_name,
488 env_name,
489 env_name
490 ))
491 }
492}
493
494impl Default for SimulationService {
495 fn default() -> Self {
496 Self::new()
497 }
498}
499
500#[derive(Debug, Clone)]
502pub struct GodotInfo {
503 pub path: String,
504 pub version: String,
505 pub in_path: bool,
506}
507
508#[derive(Debug)]
510pub struct GenerationResult {
511 pub robot_scene: PathBuf,
512 pub environments: Vec<String>,
513 pub available_count: usize,
514 pub godot_info: GodotInfo,
515}
516
517#[derive(Debug, Clone)]
519pub struct EnvironmentInfo {
520 pub id: String,
521 pub name: String,
522 pub description: String,
523 pub score: i32,
524 pub available: bool,
525}
526
527#[derive(Debug)]
529pub struct ValidationResult {
530 pub is_valid: bool,
531 pub errors: Vec<String>,
532 pub warnings: Vec<String>,
533 pub platform: String,
534 pub sensor_count: usize,
535 pub node_count: usize,
536}