celesteloader 0.5.2

Code for working with Maps, Assets, and Mods for the game Celeste
Documentation
use std::{
    collections::{BTreeMap, HashSet},
    ffi::OsStr,
    fs::File,
    io::{BufRead, BufReader},
    path::PathBuf,
};

use anyhow::{Context, Result};
use celesteloader::archive::ModArchive;

fn main() -> Result<()> {
    let respect_blacklist = false;

    let mut files = Vec::new();
    for file in std::env::args().skip(1) {
        let file = PathBuf::from(file);
        if file.is_dir() {
            let blacklist = File::open(file.join("blacklist.txt"))
                .and_then(|file| {
                    BufReader::new(file)
                        .lines()
                        .collect::<Result<HashSet<String>, _>>()
                })
                .unwrap_or_default();

            for child in file.read_dir()? {
                let child = child?.path();

                if respect_blacklist
                    && child
                        .file_name()
                        .and_then(OsStr::to_str)
                        .map_or(false, |path| blacklist.contains(path))
                {
                    continue;
                }

                files.push(child);
            }
        } else {
            files.push(file);
        }
    }

    let mut level_set_names = Vec::new();
    for path in files {
        if !path.extension().map_or(false, |e| e == "zip") {
            continue;
        }

        let reader = BufReader::new(File::open(&path)?);
        let mut archive = ModArchive::new(reader)
            .with_context(|| format!("failed to read zip {}", path.display()))?;

        let everest_yaml = archive
            .everest_yaml()
            .with_context(|| format!("no everest.yaml in {}", path.display()))?;
        let everest_name = everest_yaml.lines().find_map(|name| {
            name.split_once("Name:")
                .map(|(_, val)| val.trim().to_owned())
        });

        let dialog = archive.get_dialog("English").ok();

        let is_collab = archive.is_collab();
        let zip = path.file_name().and_then(OsStr::to_str).map(String::from);

        let mut levelsets = BTreeMap::new();
        let mut areas = BTreeMap::<String, Vec<_>>::new();

        let mut top_level_with_bins = usize::MAX;

        for path in archive
            .list_files()
            .filter_map(|path| path.strip_prefix("Maps/"))
        {
            let Some((levelset, area)) = path.rsplit_once('/') else {
                continue;
            };
            let levelset_depth = levelset.matches('/').count();
            if levelset_depth > 2 {
                continue;
            }

            let levelset_name = dialog.as_ref().and_then(|dialog| dialog.get(levelset));
            levelsets.entry(levelset).or_insert(levelset_name);

            if let Some(area) = area.strip_suffix(".bin") {
                top_level_with_bins = top_level_with_bins.min(levelset_depth);

                if let Some(area_name) = dialog
                    .as_ref()
                    .and_then(|dialog| dialog.get(&format!("{levelset}/{area}")))
                {
                    areas
                        .entry(levelset.to_owned())
                        .or_default()
                        .push(area_name);
                }
            }
        }

        level_set_names.extend(
            levelsets
                .iter()
                .filter(|&(set, ..)| {
                    let set_depth = set.matches('/').count();
                    set_depth >= top_level_with_bins
                })
                .map(|(&set, &name)| {
                    let first_levelset_name = levelsets.values().find_map(|val| *val);

                    let preferred_name = match is_collab {
                        false => name.or(first_levelset_name),
                        true => first_levelset_name.or(name),
                    };

                    let name = preferred_name.or(everest_name.as_deref()).unwrap_or(set);
                    (set.to_string(), name.to_string(), zip.clone())
                }),
        );
    }

    level_set_names.sort_by_key(|(set, ..)| set.to_ascii_lowercase());
    level_set_names.dedup_by(|(key1, name1, _), (key2, name2, _)| key1 == key2 && name1 == name2);

    for (set, name, _path) in level_set_names {
        println!(
            r#"{{ "{}", "{}" }}, "#,
            set.replace('"', "\\\""),
            name.replace('"', "\\\"")
        );
    }

    Ok(())
}