use std::{ path::Path, sync::{Arc, RwLock}, }; use delegate::delegate; use libloot::{EvalMode, MergeMode, error::DatabaseLockPoisonError}; use libloot_ffi_errors::UnsupportedEnumValueError; use crate::{ OptionalPluginMetadata, VerboseError, ffi::{EdgeType, MetadataWriteOptionsImpl}, metadata::{Group, Message, PluginMetadata, to_vec_of_unwrapped}, }; #[derive(Debug)] #[repr(transparent)] pub struct Database(Arc>); impl Database { pub fn new(db: Arc>) -> Self { Self(db) } pub fn load_masterlist(&self, path: &str) -> Result<(), VerboseError> { self.0 .write() .map_err(DatabaseLockPoisonError::from)? .load_masterlist(Path::new(path)) .map_err(Into::into) } pub fn load_masterlist_with_prelude( &self, masterlist_path: &str, prelude_path: &str, ) -> Result<(), VerboseError> { self.0 .write() .map_err(DatabaseLockPoisonError::from)? .load_masterlist_with_prelude(Path::new(masterlist_path), Path::new(prelude_path)) .map_err(Into::into) } pub fn load_userlist(&self, path: &str) -> Result<(), VerboseError> { self.0 .write() .map_err(DatabaseLockPoisonError::from)? .load_userlist(Path::new(path)) .map_err(Into::into) } pub fn write_user_metadata( &self, output_path: &str, options: MetadataWriteOptionsImpl, ) -> Result<(), VerboseError> { self.0 .read() .map_err(DatabaseLockPoisonError::from)? .write_user_metadata(Path::new(output_path), &options.into()) .map_err(Into::into) } pub fn write_minimal_list( &self, output_path: &str, options: MetadataWriteOptionsImpl, ) -> Result<(), VerboseError> { self.0 .read() .map_err(DatabaseLockPoisonError::from)? .write_minimal_list(Path::new(output_path), &options.into()) .map_err(Into::into) } pub fn evaluate(&self, condition: &str) -> Result { self.0 .read() .map_err(DatabaseLockPoisonError::from)? .evaluate(condition) .map_err(Into::into) } pub fn clear_condition_cache(&self) -> Result<(), VerboseError> { self.0 .write() .map_err(DatabaseLockPoisonError::from)? .clear_condition_cache(); Ok(()) } pub fn known_bash_tags( &self, include_user_metadata: bool, ) -> Result, VerboseError> { Ok(self .0 .read() .map_err(DatabaseLockPoisonError::from)? .known_bash_tags(to_merge_mode(include_user_metadata))) } pub fn user_known_bash_tags(&self) -> Result, VerboseError> { Ok(self .0 .read() .map_err(DatabaseLockPoisonError::from)? .user_known_bash_tags() .to_vec()) } pub fn set_user_known_bash_tags(&self, bash_tags: Vec) -> Result<(), VerboseError> { self.0 .write() .map_err(DatabaseLockPoisonError::from)? .set_user_known_bash_tags(bash_tags); Ok(()) } pub fn general_messages( &self, include_user_metadata: bool, evaluate_conditions: bool, ) -> Result, VerboseError> { self.0 .read() .map_err(DatabaseLockPoisonError::from)? .general_messages( to_merge_mode(include_user_metadata), to_eval_mode(evaluate_conditions), ) .map(|v| v.into_iter().map(Into::into).collect()) .map_err(Into::into) } pub fn user_general_messages( &self, evaluate_conditions: bool, ) -> Result, VerboseError> { Ok(self .0 .read() .map_err(DatabaseLockPoisonError::from)? .user_general_messages(to_eval_mode(evaluate_conditions))? .into_iter() .map(Into::into) .collect()) } #[expect( clippy::vec_box, reason = "Message is an opaque type to C++ so needs to be held in a Box." )] pub fn set_user_general_messages( &self, messages: Vec>, ) -> Result<(), VerboseError> { self.0 .write() .map_err(DatabaseLockPoisonError::from)? .set_user_general_messages(to_vec_of_unwrapped(messages)); Ok(()) } pub fn groups(&self, include_user_metadata: bool) -> Result, VerboseError> { Ok(self .0 .read() .map_err(DatabaseLockPoisonError::from)? .groups(to_merge_mode(include_user_metadata)) .into_iter() .map(Into::into) .collect()) } // I tried returning a GroupRef<'_> here, but it borrows from the DB read guard, which the borrow checker sees as an owned value within this scope, so won't allow me to return a reference to it. This is the only place that uses a group reference, so it's probably not worth figuring out a workaround, and cloning the groups is fine. pub fn user_groups(&self) -> Result, VerboseError> { Ok(self .0 .read() .map_err(DatabaseLockPoisonError::from)? .user_groups() .iter() .cloned() .map(Into::into) .collect()) } #[expect( clippy::vec_box, reason = "Group is an opaque type to C++ so needs to be held in a Box." )] pub fn set_user_groups(&self, groups: Vec>) -> Result<(), VerboseError> { let groups = to_vec_of_unwrapped(groups); self.0 .write() .map_err(DatabaseLockPoisonError::from)? .set_user_groups(groups); Ok(()) } pub fn groups_path( &self, from_group_name: &str, to_group_name: &str, ) -> Result, VerboseError> { self.0 .read() .map_err(DatabaseLockPoisonError::from)? .groups_path(from_group_name, to_group_name) .map(|v| v.into_iter().map(Into::into).collect()) .map_err(Into::into) } pub fn plugin_metadata( &self, plugin_name: &str, include_user_metadata: bool, evaluate_conditions: bool, ) -> Result, VerboseError> { self.0 .read() .map_err(DatabaseLockPoisonError::from)? .plugin_metadata( plugin_name, to_merge_mode(include_user_metadata), to_eval_mode(evaluate_conditions), ) .map(|p| Box::new(p.map(Into::into).into())) .map_err(Into::into) } pub fn plugin_user_metadata( &self, plugin_name: &str, evaluate_conditions: bool, ) -> Result, VerboseError> { self.0 .read() .map_err(DatabaseLockPoisonError::from)? .plugin_user_metadata(plugin_name, to_eval_mode(evaluate_conditions)) .map(|p| Box::new(p.map(Into::into).into())) .map_err(Into::into) } pub fn set_plugin_user_metadata( &mut self, plugin_metadata: Box, ) -> Result<(), VerboseError> { self.0 .write() .map_err(DatabaseLockPoisonError::from)? .set_plugin_user_metadata(plugin_metadata.into()); Ok(()) } pub fn discard_plugin_user_metadata(&self, plugin: &str) -> Result<(), VerboseError> { self.0 .write() .map_err(DatabaseLockPoisonError::from)? .discard_plugin_user_metadata(plugin); Ok(()) } pub fn discard_all_user_metadata(&self) -> Result<(), VerboseError> { self.0 .write() .map_err(DatabaseLockPoisonError::from)? .discard_all_user_metadata(); Ok(()) } } fn to_eval_mode(value: bool) -> EvalMode { if value { EvalMode::Evaluate } else { EvalMode::DoNotEvaluate } } fn to_merge_mode(value: bool) -> MergeMode { if value { MergeMode::WithUserMetadata } else { MergeMode::WithoutUserMetadata } } #[derive(Clone, Debug)] #[repr(transparent)] pub struct Vertex(libloot::Vertex); pub fn new_vertex(name: String, out_edge_type: EdgeType) -> Result, VerboseError> { let mut vertex = libloot::Vertex::new(name); if out_edge_type != EdgeType::None { vertex = vertex.with_out_edge_type(out_edge_type.try_into()?); } Ok(Box::new(Vertex(vertex))) } impl Vertex { // A value of 255 is used to indicate that there is no out edge. pub fn out_edge_type(&self) -> Result { match self.0.out_edge_type() { Some(e) => EdgeType::try_from(e).map_err(Into::into), None => Ok(EdgeType::None), } } pub fn boxed_clone(&self) -> Box { Box::new(Self(self.0.clone())) } delegate! { to self.0 { pub fn name(&self) -> &str; } } } impl From for Vertex { fn from(value: libloot::Vertex) -> Self { Self(value) } } impl TryFrom for EdgeType { type Error = UnsupportedEnumValueError; fn try_from(value: libloot::EdgeType) -> Result { match value { libloot::EdgeType::Hardcoded => Ok(EdgeType::Hardcoded), libloot::EdgeType::MasterFlag => Ok(EdgeType::MasterFlag), libloot::EdgeType::Master => Ok(EdgeType::Master), libloot::EdgeType::MasterlistRequirement => Ok(EdgeType::MasterlistRequirement), libloot::EdgeType::UserRequirement => Ok(EdgeType::UserRequirement), libloot::EdgeType::MasterlistLoadAfter => Ok(EdgeType::MasterlistLoadAfter), libloot::EdgeType::UserLoadAfter => Ok(EdgeType::UserLoadAfter), libloot::EdgeType::MasterlistGroup => Ok(EdgeType::MasterlistGroup), libloot::EdgeType::UserGroup => Ok(EdgeType::UserGroup), libloot::EdgeType::RecordOverlap => Ok(EdgeType::RecordOverlap), libloot::EdgeType::AssetOverlap => Ok(EdgeType::AssetOverlap), libloot::EdgeType::TieBreak => Ok(EdgeType::TieBreak), libloot::EdgeType::BlueprintMaster => Ok(EdgeType::BlueprintMaster), _ => Err(UnsupportedEnumValueError), } } } impl TryFrom for libloot::EdgeType { type Error = UnsupportedEnumValueError; fn try_from(value: EdgeType) -> Result { match value { EdgeType::Hardcoded => Ok(libloot::EdgeType::Hardcoded), EdgeType::MasterFlag => Ok(libloot::EdgeType::MasterFlag), EdgeType::Master => Ok(libloot::EdgeType::Master), EdgeType::MasterlistRequirement => Ok(libloot::EdgeType::MasterlistRequirement), EdgeType::UserRequirement => Ok(libloot::EdgeType::UserRequirement), EdgeType::MasterlistLoadAfter => Ok(libloot::EdgeType::MasterlistLoadAfter), EdgeType::UserLoadAfter => Ok(libloot::EdgeType::UserLoadAfter), EdgeType::MasterlistGroup => Ok(libloot::EdgeType::MasterlistGroup), EdgeType::UserGroup => Ok(libloot::EdgeType::UserGroup), EdgeType::RecordOverlap => Ok(libloot::EdgeType::RecordOverlap), EdgeType::AssetOverlap => Ok(libloot::EdgeType::AssetOverlap), EdgeType::TieBreak => Ok(libloot::EdgeType::TieBreak), EdgeType::BlueprintMaster => Ok(libloot::EdgeType::BlueprintMaster), _ => Err(UnsupportedEnumValueError), } } }