Files
fomod-manager/src/mod_config_installer.rs

257 lines
7.5 KiB
Rust

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<String, String>,
selected_files: Vec<FileTypeEnum>,
}
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<FileTypeEnum> {
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<InstallOption>,
}
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<usize>,
) -> anyhow::Result<Vec<FileTypeEnum>> {
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())
}