use anyhow::{Context, Result};
use std::collections::HashMap;
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use crate::paths;
use crate::types::SimulationConfig;
#[derive(Debug, Clone)]
pub struct SimulationPaths {
pub model_path: PathBuf,
pub environment_path: PathBuf,
pub model_config_path: Option<PathBuf>,
pub environment_config_path: Option<PathBuf>,
pub headless: bool,
pub networking: crate::types::simulation::NetworkingConfig,
}
pub struct DevService {
#[allow(dead_code)]
redis_url: String,
}
impl DevService {
pub fn new(redis_url: String) -> Self {
Self { redis_url }
}
pub async fn load_project_info(
ctx: &mut crate::context::CliContext,
) -> Result<(String, String, crate::types::ProjectConfig)> {
let project = ctx.project()?;
let name = project.name()?.to_string();
let version = project.version()?.to_string();
let config = ctx.load_project_config().await?;
Ok((name, version, config))
}
pub fn resolve_simulation_paths(
ctx: &mut crate::context::CliContext,
config: &crate::types::ProjectConfig,
) -> Result<Option<SimulationPaths>> {
if let Some(sim_config) = &config.simulation {
let sim = ctx.simulation();
if let Ok(runtime_config) =
SimulationConfig::load_with_profile_and_scenario(Some("dev"), sim_config.scenario.as_deref())
{
let model_path = sim.resolve_model_path(&runtime_config.model)?;
let env_path = sim.resolve_environment_path(&runtime_config.environment)?;
let model_config_path = sim_config.model_config.as_ref().map(|p| {
let path = PathBuf::from(p);
if path.is_absolute() {
path
} else {
std::env::current_dir().unwrap_or_default().join(path)
}
});
let env_config_path = sim_config.environment_config.as_ref().map(|p| {
let path = PathBuf::from(p);
if path.is_absolute() {
path
} else {
std::env::current_dir().unwrap_or_default().join(path)
}
});
return Ok(Some(SimulationPaths {
model_path,
environment_path: env_path,
model_config_path,
environment_config_path: env_config_path,
headless: runtime_config.godot.headless,
networking: runtime_config.networking.clone(),
}));
}
if let Some(model) = &sim_config.model {
let model_path = sim.resolve_model_path(model)?;
let env_name = sim_config
.environment
.as_deref()
.unwrap_or("@mecha10/simulation-environments/basic_arena");
let env_path = sim.resolve_environment_path(env_name)?;
let model_config_path = sim_config.model_config.as_ref().map(|p| {
let path = PathBuf::from(p);
if path.is_absolute() {
path
} else {
std::env::current_dir().unwrap_or_default().join(path)
}
});
let env_config_path = sim_config.environment_config.as_ref().map(|p| {
let path = PathBuf::from(p);
if path.is_absolute() {
path
} else {
std::env::current_dir().unwrap_or_default().join(path)
}
});
return Ok(Some(SimulationPaths {
model_path,
environment_path: env_path,
model_config_path,
environment_config_path: env_config_path,
headless: false, networking: crate::types::simulation::NetworkingConfig::default(),
}));
}
Err(anyhow::anyhow!(
"Simulation enabled but no valid configuration found.\n\
Either:\n\
1. Create configs/simulation/config.json with dev/production sections\n\
2. Use legacy format with model/environment fields in mecha10.json"
))
} else {
Ok(None)
}
}
pub fn is_port_available(&self, port: u16) -> Result<bool> {
Ok(TcpListener::bind(("127.0.0.1", port)).is_ok())
}
pub fn is_port_in_use(&self, port: u16) -> Result<bool> {
Ok(!self.is_port_available(port)?)
}
pub async fn flush_redis<C>(&self, conn: &mut C) -> Result<()>
where
C: redis::aio::ConnectionLike,
{
redis::cmd("FLUSHALL")
.query_async::<()>(conn)
.await
.context("Failed to flush Redis")?;
Ok(())
}
#[allow(dead_code)]
pub fn get_default_env_vars(&self) -> HashMap<String, String> {
let mut env = HashMap::new();
env.insert("REDIS_URL".to_string(), self.redis_url.clone());
env.insert("RUST_LOG".to_string(), "info".to_string());
env
}
#[allow(clippy::too_many_arguments)]
pub fn build_godot_args(
&self,
godot_project_path: &Path,
model_path: &Path,
env_path: &Path,
model_config_path: Option<&PathBuf>,
env_config_path: Option<&PathBuf>,
headless: bool,
networking: Option<&crate::types::simulation::NetworkingConfig>,
) -> Vec<String> {
let env_name = env_path.file_name().and_then(|n| n.to_str()).unwrap_or("environment");
let model_name = model_path.file_name().and_then(|n| n.to_str()).unwrap_or("robot");
let env_scene_path = format!("res://packages/simulation/environments/{}/{}.tscn", env_name, env_name);
let model_scene_path = format!("res://packages/simulation/models/{}/robot.tscn", model_name);
let mut args = vec!["--path".to_string(), godot_project_path.to_string_lossy().to_string()];
if headless {
args.push("--headless".to_string());
}
args.push("--".to_string());
args.push(format!("--env-path={}", env_scene_path));
args.push(format!("--model-path={}", model_scene_path));
if let Some(model_config) = model_config_path {
args.push(format!("--model-config={}", model_config.display()));
}
if let Some(env_config) = env_config_path {
args.push(format!("--env-config={}", env_config.display()));
}
if let Some(net) = networking {
args.push(format!("--protocol-port={}", net.protocol_port));
args.push(format!("--protocol-bind={}", net.protocol_bind));
args.push(format!("--camera-port={}", net.camera_port));
args.push(format!("--camera-bind={}", net.camera_bind));
args.push(format!("--signaling-port={}", net.signaling_port));
args.push(format!("--signaling-bind={}", net.signaling_bind));
}
args
}
pub fn get_godot_project_path(&self) -> PathBuf {
if let Ok(framework_path) = std::env::var("MECHA10_FRAMEWORK_PATH") {
return PathBuf::from(framework_path).join(paths::framework::SIMULATION_GODOT_DIR);
}
let cli_manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let godot_project_path = cli_manifest_dir
.parent() .map(|p| p.join("simulation/godot-project"))
.unwrap_or_else(|| PathBuf::from(paths::project::SIMULATION_GODOT_DIR));
if godot_project_path.exists() {
return godot_project_path;
}
let assets_service = crate::services::SimulationAssetsService::new();
if let Some(godot_path) = assets_service.godot_project_path() {
return godot_path;
}
PathBuf::from(paths::project::SIMULATION_GODOT_DIR)
}
}
impl Default for DevService {
fn default() -> Self {
Self::new("redis://localhost:6379".to_string())
}
}