redguard-preservation 0.6.1

A CLI tool to preserve Redguard files
Documentation
use std::path::Path;

#[derive(Debug, Clone)]
pub struct SoupDef {
    pub functions: Vec<SoupFunction>,
    pub flags: Vec<SoupFlag>,
    pub references: Vec<String>,
    pub attributes: Vec<String>,
    pub anim_groups: Vec<String>,
}

#[derive(Debug, Clone)]
pub struct SoupFunction {
    pub name: String,
    pub param_count: u8,
}

#[derive(Debug, Clone)]
pub struct SoupFlag {
    pub name: String,
    pub flag_type: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Section {
    None,
    Functions,
    Refs,
    Equates,
    Flags,
}

fn clean_line(line: &str) -> &str {
    line.split(';').next().unwrap_or("").trim()
}

#[must_use]
pub fn parse_soup_def(content: &str) -> SoupDef {
    let mut soup = SoupDef {
        functions: vec![SoupFunction {
            name: "NullFunction".to_string(),
            param_count: 0,
        }],
        flags: Vec::new(),
        references: Vec::new(),
        attributes: Vec::new(),
        anim_groups: Vec::new(),
    };

    let mut section = Section::None;
    let mut in_auto = false;
    let mut auto_index: usize = 0;
    let mut attributes_done = false;
    let mut anim_groups_done = false;
    for raw_line in content.lines() {
        let line = clean_line(raw_line);
        if line.is_empty() {
            continue;
        }

        let lower = line.to_ascii_lowercase();
        match lower.as_str() {
            "[functions]" => {
                section = Section::Functions;
                continue;
            }
            "[refs]" => {
                section = Section::Refs;
                continue;
            }
            "[equates]" => {
                section = Section::Equates;
                continue;
            }
            "[flags]" => {
                section = Section::Flags;
                continue;
            }
            _ => {}
        }

        match section {
            Section::Functions => {
                let parts = line.split_whitespace().collect::<Vec<_>>();
                if parts.len() >= 4 && parts[2].eq_ignore_ascii_case("parms") {
                    let param_count = parts[3].parse::<u8>().unwrap_or(0);
                    soup.functions.push(SoupFunction {
                        name: parts[1].to_string(),
                        param_count,
                    });
                }
            }
            Section::Refs => {
                soup.references.push(line.to_string());
            }
            Section::Equates => {
                if lower == "auto" {
                    in_auto = true;
                    auto_index = 0;
                    continue;
                }
                if lower == "endauto" {
                    if in_auto && !soup.attributes.is_empty() {
                        if attributes_done {
                            anim_groups_done = true;
                        }
                        attributes_done = true;
                    }
                    in_auto = false;
                    continue;
                }
                if in_auto && !attributes_done {
                    let parts = line.split('=').collect::<Vec<_>>();
                    let name = parts[0].trim();
                    if parts.len() < 2 {
                        while soup.attributes.len() < auto_index {
                            soup.attributes
                                .push(format!("attr_{}", soup.attributes.len()));
                        }
                        soup.attributes.push(name.to_string());
                        auto_index += 1;
                    }
                }
                if in_auto && attributes_done && !anim_groups_done {
                    let parts = line.split('=').collect::<Vec<_>>();
                    let name = parts[0].trim();
                    if parts.len() < 2 {
                        while soup.anim_groups.len() < auto_index {
                            soup.anim_groups
                                .push(format!("anim_{}", soup.anim_groups.len()));
                        }
                        soup.anim_groups.push(name.to_string());
                        auto_index += 1;
                    }
                }
            }
            Section::Flags => {
                let parts = line.split_whitespace().collect::<Vec<_>>();
                if parts.len() >= 2 {
                    soup.flags.push(SoupFlag {
                        flag_type: parts[0].to_string(),
                        name: parts[1].to_string(),
                    });
                }
            }
            Section::None => {}
        }
    }

    soup
}

fn find_case_insensitive_dir(base: &Path, target_name: &str) -> Option<std::path::PathBuf> {
    let entries = std::fs::read_dir(base).ok()?;
    for entry in entries.flatten() {
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }
        let name = path.file_name()?.to_str()?;
        if name.eq_ignore_ascii_case(target_name) {
            return Some(path);
        }
    }
    None
}

fn find_case_insensitive_file(base: &Path, target_name: &str) -> Option<std::path::PathBuf> {
    let entries = std::fs::read_dir(base).ok()?;
    for entry in entries.flatten() {
        let path = entry.path();
        if !path.is_file() {
            continue;
        }
        let name = path.file_name()?.to_str()?;
        if name.eq_ignore_ascii_case(target_name) {
            return Some(path);
        }
    }
    None
}

#[must_use]
pub fn try_load_soup_def(asset_root: &Path) -> Option<SoupDef> {
    let soup_dir = find_case_insensitive_dir(asset_root, "soup386")?;
    let def_path = find_case_insensitive_file(&soup_dir, "SOUP386.DEF")?;
    let content = std::fs::read_to_string(def_path).ok()?;
    Some(parse_soup_def(&content))
}