use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Deserialize, Serialize)]
#[allow(dead_code)] pub struct GodotConfig {
#[serde(default = "default_godot_path")]
pub executable_path: String,
#[serde(default)]
pub headless: bool,
#[serde(default = "default_viewport_width")]
pub viewport_width: u32,
#[serde(default = "default_viewport_height")]
pub viewport_height: u32,
}
fn default_godot_path() -> String {
"godot".to_string()
}
fn default_viewport_width() -> u32 {
1280
}
fn default_viewport_height() -> u32 {
720
}
impl Default for GodotConfig {
fn default() -> Self {
Self {
executable_path: default_godot_path(),
headless: false,
viewport_width: default_viewport_width(),
viewport_height: default_viewport_height(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[allow(dead_code)] pub struct CameraConfig {
#[serde(default = "default_camera_enabled")]
pub enabled: bool,
#[serde(default = "default_camera_width")]
pub width: u32,
#[serde(default = "default_camera_height")]
pub height: u32,
#[serde(default = "default_camera_fps")]
pub fps: u32,
}
fn default_camera_enabled() -> bool {
true
}
fn default_camera_width() -> u32 {
320
}
fn default_camera_height() -> u32 {
240
}
fn default_camera_fps() -> u32 {
20
}
impl Default for CameraConfig {
fn default() -> Self {
Self {
enabled: default_camera_enabled(),
width: default_camera_width(),
height: default_camera_height(),
fps: default_camera_fps(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[allow(dead_code)] pub struct NetworkingConfig {
#[serde(default = "default_protocol_port")]
pub protocol_port: u16,
#[serde(default = "default_bind_address")]
pub protocol_bind: String,
#[serde(default = "default_camera_port")]
pub camera_port: u16,
#[serde(default = "default_bind_address")]
pub camera_bind: String,
#[serde(default = "default_signaling_port")]
pub signaling_port: u16,
#[serde(default = "default_bind_address")]
pub signaling_bind: String,
}
fn default_protocol_port() -> u16 {
11008
}
fn default_camera_port() -> u16 {
11009
}
fn default_signaling_port() -> u16 {
11010
}
fn default_bind_address() -> String {
"0.0.0.0".to_string()
}
impl Default for NetworkingConfig {
fn default() -> Self {
Self {
protocol_port: default_protocol_port(),
protocol_bind: default_bind_address(),
camera_port: default_camera_port(),
camera_bind: default_bind_address(),
signaling_port: default_signaling_port(),
signaling_bind: default_bind_address(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SimulationConfig {
pub model: String,
pub model_config: Option<String>,
pub environment: String,
pub environment_config: Option<String>,
#[serde(default)]
pub godot: GodotConfig,
#[serde(default)]
pub camera: CameraConfig,
#[serde(default)]
pub networking: NetworkingConfig,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct EnvironmentSimulationConfig {
dev: Option<SimulationConfig>,
production: Option<SimulationConfig>,
}
impl SimulationConfig {
#[allow(dead_code)] pub fn load() -> anyhow::Result<Self> {
Self::load_with_profile_and_scenario(None, None)
}
pub fn load_with_profile_and_scenario(profile: Option<&str>, scenario: Option<&str>) -> anyhow::Result<Self> {
use std::env;
let profile = profile
.map(String::from)
.or_else(|| env::var("MECHA10_CONFIG_PROFILE").ok())
.or_else(|| env::var("MECHA10_ENVIRONMENT").ok())
.unwrap_or_else(|| "dev".to_string());
let scenario = scenario
.map(String::from)
.or_else(|| env::var("MECHA10_SIMULATION_SCENARIO").ok());
let base_paths = vec![
PathBuf::from("."), PathBuf::from("../.."), PathBuf::from("../../../.."), ];
for base_path in &base_paths {
let configs_dir = base_path.join("configs");
if let Some(scenario_name) = &scenario {
let scenario_path = configs_dir.join("simulation").join(format!("{}.json", scenario_name));
if scenario_path.exists() {
let content = std::fs::read_to_string(&scenario_path)?;
if let Ok(config) = Self::parse_environment_config(&content, &profile) {
return Ok(config);
}
}
}
let config_path = configs_dir.join("simulation").join("config.json");
if config_path.exists() {
let content = std::fs::read_to_string(&config_path)?;
if let Ok(config) = Self::parse_environment_config(&content, &profile) {
return Ok(config);
}
}
}
if let Some(scenario_name) = &scenario {
Err(anyhow::anyhow!(
"No simulation config found.\nTried:\n - configs/simulation/{}.json\n - configs/simulation/config.json\n\nExpected format:\n{{\n \"dev\": {{ ... }},\n \"production\": {{ ... }}\n}}",
scenario_name
))
} else {
Err(anyhow::anyhow!(
"No simulation config found.\nTried:\n - configs/simulation/config.json\n\nExpected format:\n{{\n \"dev\": {{ ... }},\n \"production\": {{ ... }}\n}}"
))
}
}
fn parse_environment_config(content: &str, profile: &str) -> anyhow::Result<Self> {
if let Ok(env_config) = serde_json::from_str::<EnvironmentSimulationConfig>(content) {
let config = match profile {
"production" | "prod" => env_config.production,
_ => env_config.dev, };
return config.ok_or_else(|| {
anyhow::anyhow!(
"No '{}' section found in simulation config. Expected format:\n{{\n \"dev\": {{ ... }},\n \"production\": {{ ... }}\n}}",
profile
)
});
}
serde_json::from_str(content).map_err(|e| anyhow::anyhow!("Failed to parse simulation config: {}", e))
}
#[allow(dead_code)] pub fn with_overrides(mut self, overrides: SimulationOverrides) -> Self {
if let Some(model) = overrides.model {
self.model = model;
}
if let Some(model_config) = overrides.model_config {
self.model_config = Some(model_config);
}
if let Some(environment) = overrides.environment {
self.environment = environment;
}
if let Some(environment_config) = overrides.environment_config {
self.environment_config = Some(environment_config);
}
if let Some(headless) = overrides.headless {
self.godot.headless = headless;
}
self
}
#[allow(dead_code)] pub fn resolve_model_path(&self, framework_path: &Path) -> PathBuf {
if self.model.starts_with("@mecha10/") {
let relative = self.model.strip_prefix("@mecha10/").unwrap();
framework_path.join("packages").join(relative)
} else {
PathBuf::from(&self.model)
}
}
#[allow(dead_code)] pub fn resolve_environment_path(&self, framework_path: &Path) -> PathBuf {
if self.environment.starts_with("@mecha10/") {
let relative = self.environment.strip_prefix("@mecha10/").unwrap();
framework_path.join("packages").join(relative)
} else {
PathBuf::from(&self.environment)
}
}
}
#[derive(Debug, Default, Clone)]
#[allow(dead_code)] pub struct SimulationOverrides {
pub model: Option<String>,
pub model_config: Option<String>,
pub environment: Option<String>,
pub environment_config: Option<String>,
pub headless: Option<bool>,
}