initial commit
This commit is contained in:
244
src/mod_config_installer.rs
Normal file
244
src/mod_config_installer.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use std::{collections::HashMap, fmt::Display};
|
||||
|
||||
use crate::fomod::{
|
||||
CompositeDependency, Config, DependencyOperator, DependencyState, FileList, FileTypeEnum,
|
||||
Group, GroupType, Plugin, PluginTypeDescriptorEnum, PluginTypeEnum,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InstallerState {
|
||||
/// Flags set by plugin selections throughout the install
|
||||
flags: HashMap<String, String>,
|
||||
|
||||
/// Files to install, keyed by destination path.
|
||||
/// Higher priority value wins when destinations conflict.
|
||||
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) {
|
||||
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 {
|
||||
return;
|
||||
};
|
||||
|
||||
self.selected_files.extend_from_slice(list);
|
||||
}
|
||||
|
||||
pub 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;
|
||||
true
|
||||
}
|
||||
|
||||
CompositeDependency::Fomm(version) => {
|
||||
// TODO: Check the fomm version
|
||||
let _ = 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GroupPrompt {
|
||||
pub name: String,
|
||||
pub select_type: GroupType,
|
||||
pub options: Vec<InstallOption>,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fn create_group_prompt(
|
||||
group: &Group,
|
||||
state: &InstallerState,
|
||||
installed_plugins: &[String],
|
||||
) -> GroupPrompt {
|
||||
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();
|
||||
|
||||
GroupPrompt {
|
||||
name: group.name.clone(),
|
||||
select_type: group.typ,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 evaluate_dependency(&dep.dependencies, state, installed_plugins) {
|
||||
return dep.typ.name;
|
||||
}
|
||||
}
|
||||
|
||||
// No dependencies was satisfied. Using default type
|
||||
dependency_plugin_type.default_type.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FomodInstaller {
|
||||
config: Config,
|
||||
installed_plugins: Vec<String>,
|
||||
group_prompt: fn(GroupPrompt) -> Vec<usize>,
|
||||
}
|
||||
|
||||
impl FomodInstaller {
|
||||
pub fn new(
|
||||
config: Config,
|
||||
installed_plugins: Vec<String>,
|
||||
group_promt: fn(GroupPrompt) -> Vec<usize>,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
installed_plugins,
|
||||
group_prompt: group_promt,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(self) -> Vec<FileTypeEnum> {
|
||||
let mut state = InstallerState::new();
|
||||
|
||||
// Always-installed files first
|
||||
if let Some(required) = &self.config.required_install_files {
|
||||
state.add_files(required);
|
||||
}
|
||||
|
||||
if let Some(install_steps) = &self.config.install_steps {
|
||||
let steps = &install_steps.install_step;
|
||||
|
||||
for (step_index, step) in steps.iter().enumerate() {
|
||||
// Check if the step should be visible
|
||||
if let Some(visible) = &step.visible {
|
||||
if !evaluate_dependency(visible, &state, &self.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 = create_group_prompt(group, &state, &self.installed_plugins);
|
||||
|
||||
let selected_plugins = (self.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) = &self.config.conditional_file_installs {
|
||||
for pattern in &conditional.patterns.pattern {
|
||||
if evaluate_dependency(&pattern.dependencies, &state, &self.installed_plugins) {
|
||||
state.add_files(&pattern.files);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.into_file_list()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user