use std::{collections::HashMap, fmt::Display}; use log::{debug, warn}; use crate::fomod::{ CompositeDependency, Config, DependencyOperator, DependencyState, FileList, FileTypeEnum, Group, GroupType, ModuleDependency, Plugin, PluginTypeDescriptorEnum, PluginTypeEnum, }; #[derive(Debug)] struct InstallerState { flags: HashMap, selected_files: Vec, } impl InstallerState { fn new() -> Self { Self { flags: HashMap::new(), selected_files: Vec::new(), } } fn set_flag(&mut self, name: &str, value: &str) { debug!("Setting flag: {} to {}", name, value); self.flags.insert(name.to_string(), value.to_string()); } fn get_flag(&self, name: &str) -> Option<&str> { self.flags.get(name).map(|s| s.as_str()) } fn add_files(&mut self, files: &FileList) { let Some(list) = &files.list else { debug!("Adding empty file list to installer state"); return; }; self.selected_files.extend_from_slice(list); } fn into_file_list(self) -> Vec { self.selected_files } } fn evaluate_dependency( dep: &CompositeDependency, state: &InstallerState, installed_plugins: &[String], ) -> bool { match dep { CompositeDependency::Flag(flag) => state .get_flag(&flag.flag) .map(|v| v == flag.value) .unwrap_or(false), CompositeDependency::File(file) => { let exists = installed_plugins .iter() .any(|p| p.eq_ignore_ascii_case(&file.file_name)); match file.state { DependencyState::Active => exists, DependencyState::Inactive => !exists, DependencyState::Missing => !exists, } } CompositeDependency::Game(version) => { // TODO: Check the game version let _ = version; warn!( "Trying to eveluate game version dependency: {} - Not implemented yet.", version.version ); true } CompositeDependency::Fomm(version) => { // TODO: Check the fomm version let _ = version; warn!( "Trying to eveluate FOMM dependency: {} - Not implemented yet.", version.version ); true } CompositeDependency::Dependency(module) => { let mut results = module .list .iter() .map(|dep| evaluate_dependency(dep, state, installed_plugins)); match module.operator { DependencyOperator::And => results.all(|r| r), DependencyOperator::Or => results.any(|r| r), } } } } fn evaluate_module_depbendecy( dep: &ModuleDependency, state: &InstallerState, installed_plugins: &[String], ) -> bool { let mut evaluated = dep .list .iter() .map(|e| evaluate_dependency(e, state, installed_plugins)); match dep.operator { DependencyOperator::And => evaluated.all(|r| r), DependencyOperator::Or => evaluated.any(|r| r), } } pub struct GroupPrompt { pub name: String, pub select_type: GroupType, pub options: Vec, } impl GroupPrompt { fn new(group: &Group, state: &InstallerState, installed_plugins: &[String]) -> Self { let options = group .plugins .plugin .iter() .enumerate() .map(|(idx, option)| InstallOption { name: option.name.trim().to_owned(), option_type: resolve_plugin_type(option, state, installed_plugins), description: option.description.trim().to_owned(), idx, }) .collect(); Self { name: group.name.clone(), select_type: group.typ, options, } } } pub struct InstallOption { pub name: String, pub option_type: PluginTypeEnum, pub idx: usize, pub description: String, } impl Display for InstallOption { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} ({:?})", self.name, self.option_type) } } /// Each plugin in the install steps have a type. e.g.: Optional, Required, Recommended /// But the type depends on dependencies /// This function eveluates the plugin type fn resolve_plugin_type( plugin: &Plugin, state: &InstallerState, installed_plugins: &[String], ) -> PluginTypeEnum { // Since the type_descriptor is optional return Optional if non is set let Some(descriptor) = &plugin.type_descriptor else { return PluginTypeEnum::Optional; }; match &descriptor.value { PluginTypeDescriptorEnum::PluginType(plugin_type) => plugin_type.name, PluginTypeDescriptorEnum::DependencyType(dependency_plugin_type) => { for dep in &dependency_plugin_type.patterns.pattern { if dep .dependencies .iter() .all(|e| evaluate_dependency(e, state, installed_plugins)) { return dep.typ.name; } } // No dependencies was satisfied. Using default type dependency_plugin_type.default_type.name } } } pub fn run_fomod_installer( fomod_config: Config, installed_plugins: &[String], group_prompt: fn(GroupPrompt) -> Vec, ) -> anyhow::Result> { let mut state = InstallerState::new(); // Always-installed files first if let Some(required) = &fomod_config.required_install_files { state.add_files(required); } if let Some(install_steps) = fomod_config.install_steps { let steps = &install_steps.install_step; for step in steps { // Check if the step should be visible if step .visible .as_ref() .is_some_and(|v| !evaluate_module_depbendecy(v, &state, installed_plugins)) { // Dependency to show the step not meet. Skipping. continue; } for group in &step.optional_file_groups.group { // TODO: Skip groups where all plugins are NotUsable let prompt = GroupPrompt::new(group, &state, installed_plugins); let selected_plugins = (group_prompt)(prompt); for i in selected_plugins { let plugin = &group.plugins.plugin[i]; // Add files from selected plugin if let Some(files) = &plugin.files { state.add_files(files); } // Set condition flags if let Some(condition_flags) = &plugin.condition_flags { for flag in &condition_flags.flag { state.set_flag(&flag.name, &flag.flag_value); } } } } } } // Evaluate conditional file installs based on final flag state if let Some(conditional) = &fomod_config.conditional_file_installs { for pattern in &conditional.patterns.pattern { if evaluate_module_depbendecy(&pattern.dependencies, &state, installed_plugins) { state.add_files(&pattern.files); } } } Ok(state.into_file_list()) }