use crate::paths;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvironmentCatalog {
pub version: String,
pub environments: Vec<Environment>,
#[serde(default)]
pub tags: HashMap<String, String>,
#[serde(default)]
pub difficulty_levels: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Environment {
pub id: String,
pub path: String,
pub name: String,
pub description: String,
pub tags: Vec<String>,
pub required_sensors: Vec<String>,
#[serde(default)]
pub optional_sensors: Vec<String>,
pub robot_platforms: Vec<String>,
#[serde(default)]
pub task_nodes: Vec<String>,
pub difficulty: String,
#[serde(default)]
pub dimensions: Vec<f32>,
#[serde(default)]
pub features: EnvironmentFeatures,
#[serde(default)]
pub curriculum_stage: i32,
#[serde(default)]
pub estimated_duration_s: i32,
#[serde(default)]
pub reward_config: HashMap<String, f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EnvironmentFeatures {
#[serde(default)]
pub floor: bool,
#[serde(default)]
pub walls: bool,
#[serde(default)]
pub obstacles: String,
#[serde(default)]
pub obstacle_count: i32,
#[serde(default)]
pub tables: i32,
#[serde(default)]
pub waypoints: i32,
#[serde(default)]
pub goal_zones: i32,
#[serde(default)]
pub npcs: bool,
#[serde(default)]
pub npc_count: i32,
#[serde(default)]
pub dynamic: bool,
}
impl EnvironmentCatalog {
pub fn load_default() -> Result<Self> {
let catalog_path = Self::default_catalog_path()?;
Self::load(&catalog_path)
}
pub fn load(path: &Path) -> Result<Self> {
let content =
fs::read_to_string(path).with_context(|| format!("Failed to read catalog file: {}", path.display()))?;
let catalog: EnvironmentCatalog =
serde_json::from_str(&content).with_context(|| "Failed to parse catalog.json")?;
Ok(catalog)
}
fn default_catalog_path() -> Result<PathBuf> {
let current_dir = std::env::current_dir()?;
let mut search_dir = current_dir.clone();
loop {
let catalog_path = search_dir
.join("packages")
.join("simulation")
.join("environments")
.join("robot-tasks")
.join("catalog.json");
if catalog_path.exists() {
return Ok(catalog_path);
}
if let Some(parent) = search_dir.parent() {
search_dir = parent.to_path_buf();
} else {
break;
}
}
Ok(PathBuf::from(paths::framework::ROBOT_TASKS_CATALOG))
}
pub fn find_by_id(&self, id: &str) -> Option<&Environment> {
self.environments.iter().find(|e| e.id == id)
}
pub fn filter_by_tag(&self, tag: &str) -> Vec<&Environment> {
self.environments
.iter()
.filter(|e| e.tags.contains(&tag.to_string()))
.collect()
}
pub fn filter_by_platform(&self, platform: &str) -> Vec<&Environment> {
self.environments
.iter()
.filter(|e| e.robot_platforms.contains(&platform.to_string()))
.collect()
}
pub fn filter_by_difficulty(&self, difficulty: &str) -> Vec<&Environment> {
self.environments
.iter()
.filter(|e| e.difficulty == difficulty)
.collect()
}
pub fn filter_by_task_node(&self, task_node: &str) -> Vec<&Environment> {
self.environments
.iter()
.filter(|e| e.task_nodes.contains(&task_node.to_string()))
.collect()
}
}
impl Environment {
pub fn is_compatible_with_sensors(&self, sensors: &[String]) -> bool {
self.required_sensors.iter().all(|required| sensors.contains(required))
}
pub fn is_compatible_with_platform(&self, platform: &str) -> bool {
self.robot_platforms.contains(&platform.to_string())
}
pub fn get_absolute_path(&self, base_path: &Path) -> PathBuf {
base_path
.join("packages")
.join("simulation")
.join("environments")
.join(&self.path)
}
}