1#![allow(dead_code)]
2
3use crate::sim::{EnvironmentSelector, RobotGenerator, RobotProfile};
9use anyhow::{Context, Result};
10use std::path::{Path, PathBuf};
11use std::process::Command;
12
13pub struct SimulationService {
45 base_path: PathBuf,
47}
48
49impl SimulationService {
50 pub fn new() -> Self {
52 Self {
53 base_path: PathBuf::from("simulation/godot"),
54 }
55 }
56
57 pub fn with_base_path(base_path: impl Into<PathBuf>) -> Self {
63 Self {
64 base_path: base_path.into(),
65 }
66 }
67
68 pub fn validate_godot(&self) -> Result<GodotInfo> {
76 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 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 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 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 let godot_info = self.validate_godot()?;
145
146 let profile = RobotProfile::from_config_file(config_path)?;
148
149 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 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 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 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 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)?; 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 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 if profile.platform.is_empty() {
243 errors.push("Platform not specified".to_string());
244 }
245
246 if profile.sensors.is_empty() {
248 warnings.push("No sensors configured".to_string());
249 }
250
251 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 pub fn run(&self, robot_name: &str, env_id: &str, headless: bool) -> Result<()> {
276 let godot_info = self.validate_godot()?;
278
279 let godot_project_path = if let Ok(framework_path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
281 PathBuf::from(framework_path).join("packages/simulation/godot-project")
283 } else {
284 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 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 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 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 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 pub fn is_setup(&self) -> bool {
352 self.base_path.exists() && self.base_path.join("robot.tscn").exists()
353 }
354
355 pub fn base_path(&self) -> &Path {
357 &self.base_path
358 }
359
360 pub fn resolve_model_path(&self, model_ref: &str) -> Result<PathBuf> {
378 let model_name = model_ref
380 .strip_prefix("@mecha10/simulation-models/")
381 .unwrap_or(model_ref);
382
383 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 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 let cli_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
401 if let Some(packages_dir) = cli_manifest_dir.parent() {
402 let bundled_model_path = packages_dir.join("simulation/models").join(model_name);
403 if bundled_model_path.exists() {
404 return Ok(bundled_model_path);
405 }
406 }
407
408 let assets_service = crate::services::SimulationAssetsService::new();
410 if let Some(models_path) = assets_service.models_path() {
411 let cached_model_path = models_path.join(model_name);
412 if cached_model_path.exists() {
413 return Ok(cached_model_path);
414 }
415 }
416
417 Err(anyhow::anyhow!(
418 "Model not found: {}\n\nChecked:\n • Project: simulation/models/{}\n • Framework: packages/simulation/models/{}\n • Cached: ~/.mecha10/simulation/current/models/{}",
419 model_ref,
420 model_name,
421 model_name,
422 model_name
423 ))
424 }
425
426 pub fn resolve_environment_path(&self, env_ref: &str) -> Result<PathBuf> {
444 let env_name = env_ref
446 .strip_prefix("@mecha10/simulation-environments/")
447 .unwrap_or(env_ref);
448
449 let project_env_path = PathBuf::from("simulation/environments").join(env_name);
451 if project_env_path.exists() {
452 return Ok(project_env_path);
453 }
454
455 if let Ok(framework_path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
457 let framework_env_path = PathBuf::from(framework_path)
458 .join("packages/simulation/environments")
459 .join(env_name);
460 if framework_env_path.exists() {
461 return Ok(framework_env_path);
462 }
463 }
464
465 let cli_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
467 if let Some(packages_dir) = cli_manifest_dir.parent() {
468 let bundled_env_path = packages_dir.join("simulation/environments").join(env_name);
469 if bundled_env_path.exists() {
470 return Ok(bundled_env_path);
471 }
472 }
473
474 let assets_service = crate::services::SimulationAssetsService::new();
476 if let Some(envs_path) = assets_service.environments_path() {
477 let cached_env_path = envs_path.join(env_name);
478 if cached_env_path.exists() {
479 return Ok(cached_env_path);
480 }
481 }
482
483 Err(anyhow::anyhow!(
484 "Environment not found: {}\n\nChecked:\n • Project: simulation/environments/{}\n • Framework: packages/simulation/environments/{}\n • Cached: ~/.mecha10/simulation/current/environments/{}",
485 env_ref,
486 env_name,
487 env_name,
488 env_name
489 ))
490 }
491}
492
493impl Default for SimulationService {
494 fn default() -> Self {
495 Self::new()
496 }
497}
498
499#[derive(Debug, Clone)]
501pub struct GodotInfo {
502 pub path: String,
503 pub version: String,
504 pub in_path: bool,
505}
506
507#[derive(Debug)]
509pub struct GenerationResult {
510 pub robot_scene: PathBuf,
511 pub environments: Vec<String>,
512 pub available_count: usize,
513 pub godot_info: GodotInfo,
514}
515
516#[derive(Debug, Clone)]
518pub struct EnvironmentInfo {
519 pub id: String,
520 pub name: String,
521 pub description: String,
522 pub score: i32,
523 pub available: bool,
524}
525
526#[derive(Debug)]
528pub struct ValidationResult {
529 pub is_valid: bool,
530 pub errors: Vec<String>,
531 pub warnings: Vec<String>,
532 pub platform: String,
533 pub sensor_count: usize,
534 pub node_count: usize,
535}