[go: up one dir, main page]

twstorage 0.2.1

Access the data and config directories of Teeworlds and DDNet
Documentation
#![cfg_attr(not(any(unix, windows)), allow(unused))]

use crate::Version;
use core::fmt;
use std::env;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;

static DDNET_CFG: OnceLock<Result<PathBuf, Error>> = OnceLock::new();
static TW_CFG: OnceLock<Result<PathBuf, Error>> = OnceLock::new();
static DDNET_DATA: OnceLock<Result<PathBuf, Error>> = OnceLock::new();
static TW_DATA: OnceLock<Result<PathBuf, Error>> = OnceLock::new();

pub fn cached_data_directory(version: Version) -> Result<&'static Path, Error> {
    let lock = match version {
        Version::DDNet06 => &DDNET_DATA,
        Version::Teeworlds07 => &TW_DATA,
    };
    match lock.get_or_init(|| find_data_directory(version)) {
        Ok(path) => Ok(path),
        Err(err) => Err(err.clone()),
    }
}

pub fn cached_config_directory(version: Version) -> Result<&'static Path, Error> {
    let lock = match version {
        Version::DDNet06 => &DDNET_CFG,
        Version::Teeworlds07 => &TW_CFG,
    };
    match lock.get_or_init(|| find_config_directory(version)) {
        Ok(path) => Ok(path),
        Err(err) => Err(err.clone()),
    }
}

#[derive(Debug, Clone)]
enum DirKind {
    Data,
    Config,
}

#[derive(Clone)]
pub struct Error {
    version: Version,
    dir: DirKind,
}

impl From<Error> for std::io::Error {
    fn from(value: Error) -> Self {
        Self::new(std::io::ErrorKind::NotFound, value)
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let game_name = match self.version {
            Version::DDNet06 => "DDNet",
            Version::Teeworlds07 => "Teeworlds",
        };
        #[cfg(unix)]
        match self.dir {
            DirKind::Data => write!(f, "Installation/Data directory of {} not found. Install the game via your packet manager or Steam. If wish to only provide the files, create a `data` directory in the current working directory or next to this program's executable.", game_name),
            DirKind::Config => write!(f, "Config directory of {} not found. Open the client once or create it yourself", game_name)
        }
        #[cfg(windows)]
        match self.dir {
            DirKind::Data => write!(f, "Installation/Data directory of {} not found. Install the game via Steam or briefly open any installed client. If wish to only provide the files, create a `data` directory in the current working directory or next to this program's executable.", game_name),
            DirKind::Config => write!(f, "Config directory of {} not found. Open the client once or create it yourself", game_name)
        }
        #[cfg(not(any(unix, windows)))]
        write!(f, "Platform not supported by twstorage")
    }
}
impl fmt::Debug for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        fmt::Display::fmt(self, f)
    }
}
impl std::error::Error for Error {}

/// This method is used in the DDNet client
fn is_valid_data_dir(path: &Path) -> bool {
    path.join("mapres").exists()
}

/// First tries the current working directory.
/// Then tries the executable's directory.
fn relative_data_dirs() -> Option<PathBuf> {
    let cwd_dir = Path::new("data");
    if is_valid_data_dir(cwd_dir) {
        return Some(PathBuf::from(cwd_dir));
    }
    if let Some(exe_dir) = env::args_os().next() {
        let mut path = PathBuf::from(exe_dir);
        if path.pop() && is_valid_data_dir(&path) {
            return Some(path);
        }
    }
    None
}

#[cfg(unix)]
fn find_data_directory(version: Version) -> Result<PathBuf, Error> {
    if let Some(path) = relative_data_dirs() {
        return Ok(path);
    }
    let distro_data_locations = match version {
        Version::DDNet06 => &[
            "/usr/share/ddnet/data",
            "/usr/share/games/ddnet/data",
            "/usr/local/share/ddnet/data",
            "/usr/local/share/games/ddnet/data",
            "/usr/pkg/share/ddnet/data",
            "/usr/pkg/share/games/ddnet/data",
            "/opt/ddnet/data",
        ],
        Version::Teeworlds07 => &[
            "/usr/share/teeworlds/data",
            "/usr/share/games/teeworlds/data",
            "/usr/local/share/teeworlds/data",
            "/usr/local/share/games/teeworlds/data",
            "/usr/pkg/share/teeworlds/data",
            "/usr/pkg/share/games/teeworlds/data",
            "/opt/teeworlds/data",
        ],
    };
    for location in distro_data_locations {
        let path = PathBuf::from(location);
        if is_valid_data_dir(&path) {
            return Ok(path);
        }
    }
    if let Some(home_dir) = env::var_os("HOME") {
        let steam_directories = &[
            ".steam/steam/steamapps/common",
            ".local/share/Steam/steamapps/common",
        ];
        let app_path = match version {
            Version::DDNet06 => "DDraceNetwork/ddnet/data",
            Version::Teeworlds07 => "Teeworlds/tw/data",
        };
        for steam_dir in steam_directories {
            let mut path = PathBuf::from(&home_dir);
            path.push(steam_dir);
            path.push(app_path);
            if is_valid_data_dir(&path) {
                return Ok(path);
            }
        }
    }
    Err(Error {
        version,
        dir: DirKind::Data,
    })
}

#[cfg(unix)]
fn find_config_directory(version: Version) -> Result<PathBuf, Error> {
    let name = match version {
        Version::DDNet06 => "ddnet",
        Version::Teeworlds07 => "teeworlds",
    };

    // Path where we will create the config directory if it does not exist yet.
    let mut create_path = None;

    if let Some(xdg_home_dir) = env::var_os("XDG_DATA_HOME") {
        let path = Path::new(&xdg_home_dir).join(name);
        if path.exists() {
            return Ok(path);
        }
        create_path.get_or_insert(path);
    }

    if let Some(home_dir) = env::var_os("HOME") {
        let path = Path::new(&home_dir).join(".local/share").join(name);
        if path.exists() {
            return Ok(path);
        }
        create_path.get_or_insert(path);

        // Also fallback to teeworlds directory for DDNet06
        let path = Path::new(&home_dir).join(".teeworlds");
        if path.exists() {
            return Ok(path);
        }
    }

    // We didn't find the directory, so we create it.
    if let Some(path) = create_path {
        if std::fs::create_dir(&path).is_ok() {
            return Ok(path);
        }
    }

    Err(Error {
        version,
        dir: DirKind::Config,
    })
}

#[cfg(windows)]
fn find_data_directory(version: Version) -> Result<PathBuf, Error> {
    if let Some(path) = relative_data_dirs() {
        return Ok(path);
    }

    let steam_dir = PathBuf::from(match version {
        Version::DDNet06 => {
            r"C:\Program Files (x86)\Steam\steamapps\common\DDraceNetwork\ddnet\data"
        }
        Version::Teeworlds07 => r"C:\Program Files (x86)\Steam\steamapps\common\Teeworlds\tw\data",
    });
    if is_valid_data_dir(&steam_dir) {
        return Ok(steam_dir);
    }

    use winreg::enums::HKEY_CURRENT_USER;
    use winreg::RegKey;

    fn get_directory_from_registry() -> Option<PathBuf> {
        let registry = RegKey::predef(HKEY_CURRENT_USER)
            .open_subkey(r"SOFTWARE\Classes\ddnet\shell\open\command")
            .ok()?;
        let command: String = registry.get_value("").ok()?;
        let path_length = command.find(".exe\"")? + 3;
        let path: String = command.chars().skip(1).take(path_length).collect();
        let mut path = PathBuf::from(path);
        if !path.pop() {
            return None;
        }
        path.push("data");
        if is_valid_data_dir(&path) {
            return Some(path);
        } else {
            None
        }
    }

    if version == Version::DDNet06 {
        if let Some(reg_dir) = get_directory_from_registry() {
            if is_valid_data_dir(&reg_dir) {
                return Ok(reg_dir);
            }
        }
    }
    Err(Error {
        version,
        dir: DirKind::Data,
    })
}

#[cfg(windows)]
fn find_config_directory(version: Version) -> Result<PathBuf, Error> {
    let appname = match version {
        Version::DDNet06 => "DDNet",
        Version::Teeworlds07 => "Teeworlds",
    };
    if let Some(appdata_dir) = env::var_os("APPDATA") {
        let path = Path::new(&appdata_dir).join(appname);
        if path.exists() || std::fs::create_dir(&path).is_ok() {
            return Ok(path);
        }
    }
    Err(Error {
        version,
        dir: DirKind::Config,
    })
}

#[cfg(not(any(unix, windows)))]
fn find_data_directory(version: Version) -> Result<PathBuf, Error> {
    Err(Error {
        version,
        dir: DirKind::Config,
    })
}

#[cfg(not(any(unix, windows)))]
fn find_config_directory(version: Version) -> Result<PathBuf, Error> {
    Err(Error {
        version,
        dir: DirKind::Config,
    })
}