#![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 {}
fn is_valid_data_dir(path: &Path) -> bool {
path.join("mapres").exists()
}
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",
};
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);
let path = Path::new(&home_dir).join(".teeworlds");
if path.exists() {
return Ok(path);
}
}
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(®_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,
})
}