use anyhow::{Context, Result};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SdlType {
Int,
Float,
Bool,
String32,
Key,
StateDescriptor,
Creatable,
Double,
Time,
Byte,
Short,
AgeTimeOfDay,
Vector3,
Point3,
Rgb,
Rgba,
Quaternion,
Rgb8,
Matrix44,
}
impl SdlType {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"INT" => Some(Self::Int),
"FLOAT" => Some(Self::Float),
"BOOL" => Some(Self::Bool),
"STRING32" => Some(Self::String32),
"PLKEY" | "KEY" => Some(Self::Key),
"STATEDESC" => Some(Self::StateDescriptor),
"CREATABLE" => Some(Self::Creatable),
"DOUBLE" => Some(Self::Double),
"TIME" => Some(Self::Time),
"BYTE" => Some(Self::Byte),
"SHORT" => Some(Self::Short),
"AGETIMEOFDAY" => Some(Self::AgeTimeOfDay),
"VECTOR3" => Some(Self::Vector3),
"POINT3" => Some(Self::Point3),
"RGB" => Some(Self::Rgb),
"RGBA" => Some(Self::Rgba),
"QUATERNION" => Some(Self::Quaternion),
"RGB8" => Some(Self::Rgb8),
"MATRIX44" => Some(Self::Matrix44),
_ => None,
}
}
pub fn byte_size(&self) -> usize {
match self {
Self::Bool | Self::Byte => 1,
Self::Short => 2,
Self::Int | Self::Float => 4,
Self::Double | Self::Time => 8,
Self::Vector3 | Self::Point3 | Self::Rgb => 12,
Self::Rgba | Self::Quaternion => 16,
Self::Rgb8 => 3,
Self::Matrix44 => 64,
Self::String32 => 32,
Self::Key | Self::StateDescriptor | Self::Creatable | Self::AgeTimeOfDay => 0,
}
}
}
#[derive(Debug, Clone)]
pub struct VarDescriptor {
pub name: String,
pub var_type: SdlType,
pub count: usize,
pub default_value: Option<String>,
pub display_option: Option<String>,
pub default_option: Option<String>,
}
#[derive(Debug, Clone)]
pub struct StateDescriptor {
pub name: String,
pub version: u32,
pub variables: Vec<VarDescriptor>,
}
pub struct SdlManager {
descriptors: HashMap<String, Vec<StateDescriptor>>,
}
impl SdlManager {
pub fn new() -> Self {
Self {
descriptors: HashMap::new(),
}
}
pub fn load_directory(&mut self, path: &Path) -> Result<usize> {
let mut count = 0;
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let file_path = entry.path();
if file_path.extension().is_some_and(|ext| ext == "sdl") {
match self.load_file(&file_path) {
Ok(n) => count += n,
Err(e) => log::warn!("Failed to load {:?}: {}", file_path, e),
}
}
}
log::info!("Loaded {} SDL descriptors from {}", count, path.display());
Ok(count)
}
pub fn load_file(&mut self, path: &Path) -> Result<usize> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read {}", path.display()))?;
let descs = parse_sdl(&content)?;
let count = descs.len();
for desc in descs {
self.descriptors
.entry(desc.name.clone())
.or_default()
.push(desc);
}
Ok(count)
}
pub fn find(&self, name: &str, version: u32) -> Option<&StateDescriptor> {
let versions = self.descriptors.get(name)?;
if version == 0 {
versions.iter().max_by_key(|d| d.version)
} else {
versions.iter().find(|d| d.version == version)
}
}
pub fn descriptor_count(&self) -> usize {
self.descriptors.values().map(|v| v.len()).sum()
}
pub fn add_descriptor(&mut self, desc: StateDescriptor) {
self.descriptors
.entry(desc.name.clone())
.or_default()
.push(desc);
}
}
#[cfg(test)]
pub(crate) fn parse_sdl_for_test(content: &str) -> Result<Vec<StateDescriptor>> {
parse_sdl(content)
}
pub fn parse_sdl(content: &str) -> Result<Vec<StateDescriptor>> {
let mut descriptors = Vec::new();
let mut current: Option<(String, u32, Vec<VarDescriptor>)> = None;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with("STATEDESC") {
let name = line.split_whitespace().nth(1).unwrap_or("").to_string();
current = Some((name, 0, Vec::new()));
continue;
}
if line == "{" {
continue;
}
if line == "}" {
if let Some((name, version, vars)) = current.take() {
descriptors.push(StateDescriptor { name, version, variables: vars });
}
continue;
}
if let Some((_, ref mut version, ref mut vars)) = current {
let tokens: Vec<&str> = line.split_whitespace().collect();
if tokens.first() == Some(&"VERSION") {
if let Some(v) = tokens.get(1) {
*version = v.parse().unwrap_or(0);
}
} else if tokens.first() == Some(&"VAR") && tokens.len() >= 3 {
let type_str = tokens[1];
let name_and_count = tokens[2];
let (var_name, count) = if let Some(bracket_pos) = name_and_count.find('[') {
let name = &name_and_count[..bracket_pos];
let count_str = &name_and_count[bracket_pos + 1..name_and_count.len() - 1];
let count = if count_str.is_empty() { 0 } else { count_str.parse().unwrap_or(1) };
(name.to_string(), count)
} else {
(name_and_count.to_string(), 1)
};
let var_type = SdlType::from_str(type_str);
let mut default_value = None;
let mut display_option = None;
let mut default_option = None;
for token in &tokens[3..] {
if let Some(val) = token.strip_prefix("DEFAULT=") {
default_value = Some(val.to_string());
} else if let Some(val) = token.strip_prefix("DEFAULTOPTION=") {
default_option = Some(val.to_string());
} else if let Some(val) = token.strip_prefix("DISPLAYOPTION=") {
display_option = Some(val.to_string());
}
}
if let Some(vt) = var_type {
vars.push(VarDescriptor {
name: var_name,
var_type: vt,
count,
default_value,
display_option,
default_option,
});
} else {
vars.push(VarDescriptor {
name: var_name,
var_type: SdlType::StateDescriptor,
count,
default_value: None,
display_option: None,
default_option: None,
});
}
}
}
}
Ok(descriptors)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_sdl() {
let content = r#"
# Comment
STATEDESC Cleft
{
VERSION 24
VAR BOOL clftWindmillLocked[1] DEFAULT=1 DISPLAYOPTION=red
VAR BYTE clftImagerPanelN[1] DEFAULT=3 DISPLAYOPTION=red
VAR BOOL clftIsCleftDone[1] DEFAULT=0 DISPLAYOPTION=red
}
"#;
let descs = parse_sdl(content).unwrap();
assert_eq!(descs.len(), 1);
assert_eq!(descs[0].name, "Cleft");
assert_eq!(descs[0].version, 24);
assert_eq!(descs[0].variables.len(), 3);
assert_eq!(descs[0].variables[0].name, "clftWindmillLocked");
assert_eq!(descs[0].variables[0].var_type, SdlType::Bool);
assert_eq!(descs[0].variables[0].count, 1);
assert_eq!(descs[0].variables[0].default_value.as_deref(), Some("1"));
assert_eq!(descs[0].variables[1].var_type, SdlType::Byte);
}
#[test]
fn test_sdl_manager() {
let mut mgr = SdlManager::new();
let content = "STATEDESC Test\n{\nVERSION 5\nVAR INT foo[1] DEFAULT=42\n}\n";
let descs = parse_sdl(content).unwrap();
for d in descs {
mgr.descriptors.entry(d.name.clone()).or_default().push(d);
}
let found = mgr.find("Test", 0).unwrap();
assert_eq!(found.version, 5);
assert_eq!(found.variables[0].name, "foo");
}
}