//! Module to interact with Minetest worlds (savegames). //! //! The main object is [`World`], which represents a Minetest world on-disk. use std::{ collections::HashMap, fmt, path::{Path, PathBuf}, }; use super::{ error::{Error, Result}, kvstore, minemod::ModId, }; /// Represents an on-disk Minetest world. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct World { path: PathBuf, } impl World { /// Open the given directory as a [`World`]. pub fn open>(path: P) -> Result { World::open_path(path.as_ref()) } fn open_path(path: &Path) -> Result { let conf = path.join("world.mt"); if !conf.is_file() { return Err(Error::InvalidWorldDir(path.into())); } Ok(World { path: path.into() }) } fn conf(&self) -> Result> { let conf = self.path.join("world.mt"); kvstore::read(&conf) } /// Returns the name of the world. pub fn world_name(&self) -> Result { let fallback = self .path .file_name() .map(|s| s.to_str().expect("Non-UTF8 directory encountered")); let conf = self.conf()?; conf.get("world_name") .map(String::as_str) .or(fallback) .ok_or_else(|| Error::InvalidWorldDir(self.path.clone())) .map(Into::into) } /// Extract the game that this world uses. pub fn game_id(&self) -> Result { let conf = self.conf()?; conf.get("gameid").ok_or(Error::NoGameSet).map(Into::into) } /// Returns all mods that are loaded in this world. /// /// This returns mods that are explicitely loaded in the config, but not mods that are loaded /// through the game. pub fn mods(&self) -> Result> { let conf = self.conf()?; const PREFIX_LEN: usize = "load_mod_".len(); Ok(conf .iter() .filter(|(k, _)| k.starts_with("load_mod_")) .filter(|(_, v)| *v == "true") .map(|i| i.0[PREFIX_LEN..].into()) .collect()) } /// Enable the given mod. /// /// Note that this function does not ensure that the mod exists on-disk, nor does it do any /// dependency checks. It simply adds the right `load_mod`-line to the world configuration /// file. pub fn enable_mod(&self, mod_id: &str) -> Result<()> { let mut conf = self.conf()?; let key = format!("load_mod_{}", mod_id); conf.entry(key) .and_modify(|e| *e = "true".into()) .or_insert_with(|| "true".into()); kvstore::write(&conf, &self.path.join("world.mt")) } } impl fmt::Display for World { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.world_name().map_err(|_| fmt::Error)?) } }