mecha10_cli/types/
simulation.rs1use serde::{Deserialize, Serialize};
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone, Deserialize, Serialize)]
11#[allow(dead_code)] pub struct GodotConfig {
13 #[serde(default = "default_godot_path")]
15 pub executable_path: String,
16
17 #[serde(default)]
19 pub headless: bool,
20
21 #[serde(default = "default_viewport_width")]
23 pub viewport_width: u32,
24
25 #[serde(default = "default_viewport_height")]
27 pub viewport_height: u32,
28}
29
30fn default_godot_path() -> String {
31 "godot".to_string()
32}
33
34fn default_viewport_width() -> u32 {
35 1280
36}
37
38fn default_viewport_height() -> u32 {
39 720
40}
41
42impl Default for GodotConfig {
43 fn default() -> Self {
44 Self {
45 executable_path: default_godot_path(),
46 headless: false,
47 viewport_width: default_viewport_width(),
48 viewport_height: default_viewport_height(),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Deserialize, Serialize)]
55#[allow(dead_code)] pub struct CameraConfig {
57 #[serde(default = "default_camera_enabled")]
59 pub enabled: bool,
60
61 #[serde(default = "default_camera_width")]
63 pub width: u32,
64
65 #[serde(default = "default_camera_height")]
67 pub height: u32,
68
69 #[serde(default = "default_camera_fps")]
71 pub fps: u32,
72}
73
74fn default_camera_enabled() -> bool {
75 true
76}
77
78fn default_camera_width() -> u32 {
79 320
80}
81
82fn default_camera_height() -> u32 {
83 240
84}
85
86fn default_camera_fps() -> u32 {
87 20
88}
89
90impl Default for CameraConfig {
91 fn default() -> Self {
92 Self {
93 enabled: default_camera_enabled(),
94 width: default_camera_width(),
95 height: default_camera_height(),
96 fps: default_camera_fps(),
97 }
98 }
99}
100
101#[derive(Debug, Clone, Deserialize, Serialize)]
103#[allow(dead_code)] pub struct NetworkingConfig {
105 #[serde(default = "default_protocol_port")]
107 pub protocol_port: u16,
108
109 #[serde(default = "default_bind_address")]
111 pub protocol_bind: String,
112
113 #[serde(default = "default_camera_port")]
115 pub camera_port: u16,
116
117 #[serde(default = "default_bind_address")]
119 pub camera_bind: String,
120
121 #[serde(default = "default_signaling_port")]
123 pub signaling_port: u16,
124
125 #[serde(default = "default_bind_address")]
127 pub signaling_bind: String,
128}
129
130fn default_protocol_port() -> u16 {
131 11008
132}
133
134fn default_camera_port() -> u16 {
135 11009
136}
137
138fn default_signaling_port() -> u16 {
139 11010
140}
141
142fn default_bind_address() -> String {
143 "0.0.0.0".to_string()
144}
145
146impl Default for NetworkingConfig {
147 fn default() -> Self {
148 Self {
149 protocol_port: default_protocol_port(),
150 protocol_bind: default_bind_address(),
151 camera_port: default_camera_port(),
152 camera_bind: default_bind_address(),
153 signaling_port: default_signaling_port(),
154 signaling_bind: default_bind_address(),
155 }
156 }
157}
158
159#[derive(Debug, Clone, Deserialize, Serialize)]
171pub struct SimulationConfig {
172 pub model: String,
174
175 pub model_config: Option<String>,
177
178 pub environment: String,
180
181 pub environment_config: Option<String>,
183
184 #[serde(default)]
186 pub godot: GodotConfig,
187
188 #[serde(default)]
190 pub camera: CameraConfig,
191
192 #[serde(default)]
194 pub networking: NetworkingConfig,
195}
196
197#[derive(Debug, Clone, Deserialize, Serialize)]
202struct EnvironmentSimulationConfig {
203 dev: Option<SimulationConfig>,
204 production: Option<SimulationConfig>,
205}
206
207impl SimulationConfig {
208 #[allow(dead_code)] pub fn load() -> anyhow::Result<Self> {
216 Self::load_with_profile_and_scenario(None, None)
217 }
218
219 pub fn load_with_profile_and_scenario(profile: Option<&str>, scenario: Option<&str>) -> anyhow::Result<Self> {
233 use std::env;
234
235 let profile = profile
237 .map(String::from)
238 .or_else(|| env::var("MECHA10_CONFIG_PROFILE").ok())
239 .or_else(|| env::var("MECHA10_ENVIRONMENT").ok())
240 .unwrap_or_else(|| "dev".to_string());
241
242 let scenario = scenario
244 .map(String::from)
245 .or_else(|| env::var("MECHA10_SIMULATION_SCENARIO").ok());
246
247 let base_paths = vec![
249 PathBuf::from("."), PathBuf::from("../.."), PathBuf::from("../../../.."), ];
253
254 for base_path in &base_paths {
256 let configs_dir = base_path.join("configs");
257
258 if let Some(scenario_name) = &scenario {
260 let scenario_path = configs_dir.join("simulation").join(format!("{}.json", scenario_name));
261
262 if scenario_path.exists() {
263 let content = std::fs::read_to_string(&scenario_path)?;
264 if let Ok(config) = Self::parse_environment_config(&content, &profile) {
266 return Ok(config);
267 }
268 }
269 }
270
271 let config_path = configs_dir.join("simulation").join("config.json");
273
274 if config_path.exists() {
275 let content = std::fs::read_to_string(&config_path)?;
276 if let Ok(config) = Self::parse_environment_config(&content, &profile) {
277 return Ok(config);
278 }
279 }
280 }
281
282 if let Some(scenario_name) = &scenario {
283 Err(anyhow::anyhow!(
284 "No simulation config found.\nTried:\n - configs/simulation/{}.json\n - configs/simulation/config.json\n\nExpected format:\n{{\n \"dev\": {{ ... }},\n \"production\": {{ ... }}\n}}",
285 scenario_name
286 ))
287 } else {
288 Err(anyhow::anyhow!(
289 "No simulation config found.\nTried:\n - configs/simulation/config.json\n\nExpected format:\n{{\n \"dev\": {{ ... }},\n \"production\": {{ ... }}\n}}"
290 ))
291 }
292 }
293
294 fn parse_environment_config(content: &str, profile: &str) -> anyhow::Result<Self> {
296 if let Ok(env_config) = serde_json::from_str::<EnvironmentSimulationConfig>(content) {
298 let config = match profile {
300 "production" | "prod" => env_config.production,
301 _ => env_config.dev, };
303
304 return config.ok_or_else(|| {
305 anyhow::anyhow!(
306 "No '{}' section found in simulation config. Expected format:\n{{\n \"dev\": {{ ... }},\n \"production\": {{ ... }}\n}}",
307 profile
308 )
309 });
310 }
311
312 serde_json::from_str(content).map_err(|e| anyhow::anyhow!("Failed to parse simulation config: {}", e))
314 }
315
316 #[allow(dead_code)] pub fn with_overrides(mut self, overrides: SimulationOverrides) -> Self {
321 if let Some(model) = overrides.model {
322 self.model = model;
323 }
324 if let Some(model_config) = overrides.model_config {
325 self.model_config = Some(model_config);
326 }
327 if let Some(environment) = overrides.environment {
328 self.environment = environment;
329 }
330 if let Some(environment_config) = overrides.environment_config {
331 self.environment_config = Some(environment_config);
332 }
333 if let Some(headless) = overrides.headless {
334 self.godot.headless = headless;
335 }
336 self
337 }
338
339 #[allow(dead_code)] pub fn resolve_model_path(&self, framework_path: &Path) -> PathBuf {
344 if self.model.starts_with("@mecha10/") {
345 let relative = self.model.strip_prefix("@mecha10/").unwrap();
346 framework_path.join("packages").join(relative)
347 } else {
348 PathBuf::from(&self.model)
349 }
350 }
351
352 #[allow(dead_code)] pub fn resolve_environment_path(&self, framework_path: &Path) -> PathBuf {
357 if self.environment.starts_with("@mecha10/") {
358 let relative = self.environment.strip_prefix("@mecha10/").unwrap();
359 framework_path.join("packages").join(relative)
360 } else {
361 PathBuf::from(&self.environment)
362 }
363 }
364}
365
366#[derive(Debug, Default, Clone)]
368#[allow(dead_code)] pub struct SimulationOverrides {
370 pub model: Option<String>,
371 pub model_config: Option<String>,
372 pub environment: Option<String>,
373 pub environment_config: Option<String>,
374 pub headless: Option<bool>,
375}