initial commit

This commit is contained in:
2026-02-26 17:08:38 +01:00
commit c34a957a9d
8 changed files with 854 additions and 0 deletions

244
src/mod_config_installer.rs Normal file
View 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()
}
}