257 lines
7.5 KiB
Rust
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())
|
|
}
|