termcraft 0.1.0

Terminal-based 2D sandbox survival game
Documentation
use std::path::{Path, PathBuf};
use std::sync::Once;

pub const LEGACY_SAVE_DIR: &str = "saves";
#[cfg(not(test))]
const APP_DIR_NAME: &str = "termcraft";
#[cfg(not(test))]
const SAVE_SUBDIR: &str = "saves";
const SAVE_DIR_ENV: &str = "TERMCRAFT_SAVE_DIR";
static MIGRATE_LEGACY_SAVES: Once = Once::new();

pub fn save_dir() -> PathBuf {
    explicit_save_dir().unwrap_or_else(default_save_dir)
}

pub fn save_file(file_name: &str) -> PathBuf {
    save_dir().join(file_name)
}

pub fn migrate_legacy_saves_once() {
    MIGRATE_LEGACY_SAVES.call_once(|| {
        if cfg!(test) || explicit_save_dir().is_some() {
            return;
        }

        let legacy_dir = PathBuf::from(LEGACY_SAVE_DIR);
        let target_dir = save_dir();
        if !legacy_dir.is_dir() || paths_are_same(&legacy_dir, &target_dir) {
            return;
        }

        let _ = copy_missing_files(&legacy_dir, &target_dir);
    });
}

fn explicit_save_dir() -> Option<PathBuf> {
    std::env::var_os(SAVE_DIR_ENV)
        .filter(|path| !path.is_empty())
        .map(PathBuf::from)
}

#[cfg(test)]
fn default_save_dir() -> PathBuf {
    PathBuf::from(LEGACY_SAVE_DIR)
}

#[cfg(all(not(test), target_os = "windows"))]
fn default_save_dir() -> PathBuf {
    env_path("APPDATA")
        .or_else(|| env_path("USERPROFILE").map(|home| home.join("AppData").join("Roaming")))
        .map(|base| base.join(APP_DIR_NAME).join(SAVE_SUBDIR))
        .unwrap_or_else(|| PathBuf::from(LEGACY_SAVE_DIR))
}

#[cfg(all(not(test), target_os = "macos"))]
fn default_save_dir() -> PathBuf {
    env_path("HOME")
        .map(|home| {
            home.join("Library")
                .join("Application Support")
                .join(APP_DIR_NAME)
                .join(SAVE_SUBDIR)
        })
        .unwrap_or_else(|| PathBuf::from(LEGACY_SAVE_DIR))
}

#[cfg(all(not(test), unix, not(target_os = "macos")))]
fn default_save_dir() -> PathBuf {
    env_path("XDG_DATA_HOME")
        .or_else(|| env_path("HOME").map(|home| home.join(".local").join("share")))
        .map(|base| base.join(APP_DIR_NAME).join(SAVE_SUBDIR))
        .unwrap_or_else(|| PathBuf::from(LEGACY_SAVE_DIR))
}

#[cfg(all(not(test), not(unix), not(target_os = "windows")))]
fn default_save_dir() -> PathBuf {
    PathBuf::from(LEGACY_SAVE_DIR)
}

#[cfg(not(test))]
fn env_path(name: &str) -> Option<PathBuf> {
    std::env::var_os(name)
        .filter(|path| !path.is_empty())
        .map(PathBuf::from)
}

fn paths_are_same(a: &Path, b: &Path) -> bool {
    match (a.canonicalize(), b.canonicalize()) {
        (Ok(a), Ok(b)) => a == b,
        _ => false,
    }
}

fn copy_missing_files(source: &Path, target: &Path) -> std::io::Result<()> {
    std::fs::create_dir_all(target)?;
    for entry in std::fs::read_dir(source)? {
        let entry = entry?;
        let source_path = entry.path();
        let target_path = target.join(entry.file_name());
        let file_type = entry.file_type()?;
        if file_type.is_dir() {
            copy_missing_files(&source_path, &target_path)?;
        } else if file_type.is_file() && !is_temp_save_file(&source_path) && !target_path.exists() {
            std::fs::copy(source_path, target_path)?;
        }
    }
    Ok(())
}

fn is_temp_save_file(path: &Path) -> bool {
    path.file_name()
        .and_then(|name| name.to_str())
        .is_some_and(|name| name.ends_with(".tmp"))
}