use std::{ any::Any, collections::HashMap, fmt, fs, path::{Path, PathBuf}, }; use fs_extra::dir::{self, CopyOptions}; use super::{ error::{Error, Result}, kvstore, scan, }; /// The type of the ID that is used to identify Minetest mods. pub type ModId = String; /// A minemod is a mod that is saved somewhere on disk. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct MineMod { path: PathBuf, } impl MineMod { pub fn open>(path: P) -> Result { MineMod::open_path(path.as_ref()) } fn open_path(path: &Path) -> Result { let conf = path.join("mod.conf"); if !conf.is_file() { return Err(Error::InvalidModDir(path.into())); } Ok(MineMod { path: path.into() }) } fn read_conf(&self) -> Result> { let conf = self.path.join("mod.conf"); kvstore::read(&conf) } /// Read the mod ID. pub fn mod_id(&self) -> Result { let conf = self.read_conf()?; conf.get("name") .map(Into::into) .ok_or_else(|| Error::InvalidModDir(self.path.clone())) } /// Returns all dependencies of this mod. pub fn dependencies(&self) -> Result> { let conf = self.read_conf()?; static EMPTY: String = String::new(); let depstr = conf.get("depends").unwrap_or(&EMPTY); Ok(depstr .split(',') .map(str::trim) .filter(|s| !s.is_empty()) .map(Into::into) .collect()) } /// Copies the mod to the given path. /// /// Note that the path should not include the mod directory, that will be appended /// automatically. /// /// Returns a new [`MineMod`] object pointing to the copy. pub fn copy_to>(&self, path: P) -> Result { let mut options = CopyOptions::new(); options.content_only = true; let path = path.as_ref().join(self.mod_id()?); fs::create_dir_all(&path)?; dir::copy(&self.path, &path, &options)?; MineMod::open(&path) } } impl fmt::Display for MineMod { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.mod_id().map_err(|_| fmt::Error)?) } } /// Represents an on-disk modpack. /// /// We don't support many modpack operations besides listing the modpack contents. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Modpack { path: PathBuf, } impl Modpack { pub fn open>(path: P) -> Result { Modpack::open_path(path.as_ref()) } fn open_path(path: &Path) -> Result { let conf = path.join("modpack.conf"); if !conf.is_file() { return Err(Error::InvalidModpackDir(path.into())); } Ok(Modpack { path: path.into() }) } fn conf(&self) -> Result> { let conf = self.path.join("modpack.conf"); kvstore::read(&conf) } /// Returns the name of the modpack. pub fn name(&self) -> Result { self.conf()? .get("name") .map(Into::into) .ok_or_else(|| Error::InvalidModDir(self.path.clone())) } /// Return all mods contained in this modpack. pub fn mods(&self) -> Result> { let mut mods = vec![]; for container in scan(&self.path, |p| open_mod_or_pack(p))? { mods.extend(container.mods()?); } Ok(mods) } /// Copies the modpack to the given path. /// /// Note that the path should not include the modpack directory, that will be appended /// automatically. /// /// Returns a new [`Modpack`] object pointing to the copy. pub fn copy_to>(&self, path: P) -> Result { let mut options = CopyOptions::new(); options.content_only = true; let path = path.as_ref().join(self.name()?); fs::create_dir_all(&path)?; dir::copy(&self.path, &path, &options)?; Modpack::open(&path) } } impl fmt::Display for Modpack { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} (pack)", self.name().map_err(|_| fmt::Error)?) } } /// A thing that can contain mods. pub trait ModContainer: Any + fmt::Display { /// Returns the name of the mod container. fn name(&self) -> Result; /// Return all contained mods. fn mods(&self) -> Result>; /// Copies the content to the given directory. fn install_to(&self, path: &Path) -> Result>; } impl ModContainer for MineMod { fn name(&self) -> Result { self.mod_id() } fn mods(&self) -> Result> { Ok(vec![self.clone()]) } fn install_to(&self, path: &Path) -> Result> { self.copy_to(path) .map(|x| Box::new(x) as Box) } } impl ModContainer for Modpack { fn name(&self) -> Result { self.name() } fn mods(&self) -> Result> { self.mods() } fn install_to(&self, path: &Path) -> Result> { self.copy_to(path) .map(|x| Box::new(x) as Box) } } /// Attempts to open the given path as either a single mod or a modpack. pub fn open_mod_or_pack>(path: P) -> Result> { MineMod::open(path.as_ref()) .map(|m| Box::new(m) as Box) .or_else(|_| Modpack::open(path.as_ref()).map(|p| Box::new(p) as Box)) }