fomod/
lib.rs

1pub mod spec;
2
3use std::io::BufReader;
4
5use quick_xml::DeError;
6
7pub use crate::spec::{
8    types::{
9        FileDependency, FileType, FlagDependency, HeaderImage, PluginTypeEnum, SetConditionFlag,
10        VersionDependency,
11    },
12    Info,
13};
14
15use crate::spec::Config as SpecConfig;
16
17#[derive(Debug, PartialEq)]
18pub struct Config {
19    pub module_name: String,
20    pub module_image: Option<HeaderImage>,
21    pub module_dependencies: Option<DependencyOperator<Dependency>>,
22    pub required_install_files: Vec<FileType>,
23    pub install_steps: OrderEnum<InstallStep>,
24    pub conditional_file_installs: Vec<ConditionalInstallPattern>,
25}
26impl From<SpecConfig> for Config {
27    fn from(spec: SpecConfig) -> Self {
28        let mut conditional_file_installs = Vec::new();
29
30        conditional_file_installs.extend(
31            spec.conditional_file_installs
32                .map(|cfi| {
33                    cfi.patterns
34                        .pattern
35                        .iter()
36                        .map(|cfi| ConditionalInstallPattern::from(cfi.clone()))
37                        .collect::<Vec<ConditionalInstallPattern>>()
38                })
39                .unwrap_or_default(),
40        );
41
42        Self {
43            module_name: spec.module_name,
44            module_image: spec.module_image,
45            module_dependencies: spec
46                .module_dependencies
47                .map(|md| DependencyOperator::from(md)),
48            required_install_files: spec
49                .required_install_files
50                .map(|rif| rif.list)
51                .flatten()
52                .unwrap_or_default(),
53            install_steps: spec
54                .install_steps
55                .map(|is| OrderEnum::from(is))
56                .unwrap_or_default(),
57            conditional_file_installs,
58        }
59    }
60}
61impl TryFrom<&str> for Config {
62    type Error = DeError;
63
64    fn try_from(string: &str) -> Result<Self, Self::Error> {
65        Ok(Self::from(SpecConfig::try_from(string)?))
66    }
67}
68impl<T> TryFrom<BufReader<T>> for Config
69where
70    T: std::io::Read,
71{
72    type Error = DeError;
73
74    fn try_from(reader: BufReader<T>) -> Result<Self, Self::Error> {
75        Ok(Self::from(SpecConfig::try_from(reader)?))
76    }
77}
78
79#[derive(Clone, Debug, PartialEq, Eq)]
80pub enum Dependency {
81    File(FileDependency),
82    Flag(FlagDependency),
83    Game(VersionDependency),
84    Fomm(VersionDependency),
85    Dependency(DependencyOperator<Self>),
86}
87impl From<crate::spec::types::CompositeDependency> for Dependency {
88    fn from(comp_dep: crate::spec::types::CompositeDependency) -> Self {
89        use crate::spec::types::CompositeDependency;
90
91        match comp_dep {
92            CompositeDependency::File(f) => Self::File(f),
93            CompositeDependency::Flag(f) => Self::Flag(f),
94            CompositeDependency::Game(v) => Self::Game(v),
95            CompositeDependency::Fomm(v) => Self::Fomm(v),
96            CompositeDependency::Dependency(f) => Self::Dependency(DependencyOperator::from(f)),
97        }
98    }
99}
100
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub enum DependencyOperator<T> {
103    And(Vec<T>),
104    Or(Vec<T>),
105}
106impl From<crate::spec::types::ModuleDependency> for DependencyOperator<Dependency> {
107    fn from(mod_dep: crate::spec::types::ModuleDependency) -> Self {
108        use crate::spec::types::DependencyOperator as DepOp;
109
110        let mut list = Vec::new();
111        for cd in mod_dep.list {
112            list.push(Dependency::from(cd));
113        }
114
115        match mod_dep.operator {
116            DepOp::And => DependencyOperator::And(list),
117            DepOp::Or => DependencyOperator::Or(list),
118        }
119    }
120}
121
122#[derive(Clone, Debug, PartialEq, Eq)]
123pub enum OrderEnum<T> {
124    Ascending(Vec<T>),
125    Explicit(Vec<T>),
126    Descending(Vec<T>),
127}
128impl<T> OrderEnum<T>
129where
130    T: Ord,
131    T: Clone,
132{
133    pub fn vec_sorted(&self) -> Vec<T> {
134        match self {
135            Self::Ascending(v) => {
136                let mut v = v.clone();
137                v.sort();
138                v
139            }
140            Self::Explicit(v) => v.clone(),
141            Self::Descending(v) => {
142                //FIXME Sort Descending
143                let mut v = v.clone();
144                v.sort();
145                v
146            }
147        }
148    }
149    pub fn vec_sorted_mut(&mut self) -> &mut Vec<T> {
150        match self {
151            Self::Ascending(v) => {
152                v.sort();
153                v
154            }
155            Self::Explicit(v) => v,
156            Self::Descending(v) => {
157                //FIXME Sort Descending
158                v.sort();
159                v
160            }
161        }
162    }
163}
164impl<T> Default for OrderEnum<T> {
165    fn default() -> Self {
166        Self::Ascending(Vec::new())
167    }
168}
169impl From<spec::types::StepList> for OrderEnum<InstallStep> {
170    fn from(step_list: spec::types::StepList) -> Self {
171        let mut list = Vec::new();
172        list.extend(
173            step_list
174                .install_step
175                .iter()
176                .map(|is| InstallStep::from(is.clone())),
177        );
178
179        use spec::types::OrderEnum;
180        match step_list.order {
181            OrderEnum::Ascending => Self::Ascending(list),
182            OrderEnum::Explicit => Self::Explicit(list),
183            OrderEnum::Descending => Self::Descending(list),
184        }
185    }
186}
187impl From<spec::types::GroupList> for OrderEnum<Group> {
188    fn from(group_list: spec::types::GroupList) -> Self {
189        let mut list = Vec::new();
190        list.extend(group_list.group.iter().map(|is| Group::from(is.clone())));
191
192        use spec::types::OrderEnum;
193        match group_list.order {
194            OrderEnum::Ascending => Self::Ascending(list),
195            OrderEnum::Explicit => Self::Explicit(list),
196            OrderEnum::Descending => Self::Descending(list),
197        }
198    }
199}
200impl From<spec::types::PluginList> for OrderEnum<Plugin> {
201    fn from(plugin_list: spec::types::PluginList) -> Self {
202        let mut list = Vec::new();
203        list.extend(plugin_list.plugin.iter().map(|is| Plugin::from(is.clone())));
204
205        use spec::types::OrderEnum;
206        match plugin_list.order {
207            OrderEnum::Ascending => Self::Ascending(list),
208            OrderEnum::Explicit => Self::Explicit(list),
209            OrderEnum::Descending => Self::Descending(list),
210        }
211    }
212}
213
214#[derive(Clone, Debug, PartialEq, Eq)]
215pub struct InstallStep {
216    pub name: String,
217    pub visible: Option<Dependency>,
218    pub optional_file_groups: OrderEnum<Group>,
219}
220impl PartialOrd for InstallStep {
221    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
222        self.name.as_str().partial_cmp(other.name.as_str())
223    }
224}
225impl Ord for InstallStep {
226    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
227        self.name.as_str().cmp(other.name.as_str())
228    }
229}
230impl From<spec::types::InstallStep> for InstallStep {
231    fn from(install_step: spec::types::InstallStep) -> Self {
232        Self {
233            name: install_step.name,
234            visible: install_step.visible.map(|v| Dependency::from(v)),
235            optional_file_groups: OrderEnum::from(install_step.optional_file_groups),
236        }
237    }
238}
239
240#[derive(Clone, Debug, PartialEq, Eq)]
241pub enum GroupType<T> {
242    SelectAtLeastOne(T),
243    SelectAtMostOne(T),
244    SelectExactlyOne(T),
245    SelectAll(T),
246    SelectAny(T),
247}
248impl From<(spec::types::GroupType, spec::types::PluginList)> for GroupType<OrderEnum<Plugin>> {
249    fn from((gt, pl): (spec::types::GroupType, spec::types::PluginList)) -> Self {
250        let oe = OrderEnum::from(pl);
251
252        use spec::types::GroupType;
253        match gt {
254            GroupType::SelectAtLeastOne => Self::SelectAtLeastOne(oe),
255            GroupType::SelectAtMostOne => Self::SelectAtMostOne(oe),
256            GroupType::SelectExactlyOne => Self::SelectExactlyOne(oe),
257            GroupType::SelectAll => Self::SelectAll(oe),
258            GroupType::SelectAny => Self::SelectAny(oe),
259        }
260    }
261}
262
263#[derive(Clone, Debug, PartialEq, Eq)]
264pub struct Group {
265    pub name: String,
266    pub plugins: GroupType<OrderEnum<Plugin>>,
267}
268impl From<spec::types::Group> for Group {
269    fn from(group: spec::types::Group) -> Self {
270        Self {
271            name: group.name,
272            plugins: GroupType::from((group.typ, group.plugins)),
273        }
274    }
275}
276impl PartialOrd for Group {
277    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
278        self.name.as_str().partial_cmp(other.name.as_str())
279    }
280}
281impl Ord for Group {
282    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
283        self.name.as_str().cmp(other.name.as_str())
284    }
285}
286
287#[derive(Clone, Debug, PartialEq, Eq)]
288pub struct Plugin {
289    pub name: String,
290    pub description: String,
291    pub image: Option<String>,
292
293    pub files: Vec<FileType>,
294    pub condition_flags: Vec<SetConditionFlag>,
295    pub type_descriptor: Option<PluginTypeDescriptorEnum>,
296}
297impl From<spec::types::Plugin> for Plugin {
298    fn from(plugin: spec::types::Plugin) -> Self {
299        Self {
300            name: plugin.name,
301            description: plugin.description,
302            image: plugin.image.map(|i| i.path),
303            files: plugin.files.map(|fl| fl.list).flatten().unwrap_or_default(),
304            condition_flags: plugin
305                .condition_flags
306                .map(|cfl| cfl.flag)
307                .unwrap_or_default(),
308            type_descriptor: plugin
309                .type_descriptor
310                .map(|td| PluginTypeDescriptorEnum::from(td)),
311        }
312    }
313}
314impl PartialOrd for Plugin {
315    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
316        self.name.as_str().partial_cmp(other.name.as_str())
317    }
318}
319impl Ord for Plugin {
320    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
321        self.name.as_str().cmp(other.name.as_str())
322    }
323}
324
325#[derive(Clone, Debug, PartialEq, Eq)]
326pub enum PluginTypeDescriptorEnum {
327    DependencyType(Vec<DependencyPattern>),
328    PluginType(PluginTypeEnum),
329}
330impl From<spec::types::PluginTypeDescriptorEnum> for PluginTypeDescriptorEnum {
331    fn from(ptde: spec::types::PluginTypeDescriptorEnum) -> Self {
332        use spec::types::PluginTypeDescriptorEnum;
333        match ptde {
334            PluginTypeDescriptorEnum::DependencyType(dpt) => {
335                //FIXME: DependencyPluginType::default_type not accounted for!!
336                let mut list = Vec::new();
337                list.extend(
338                    dpt.patterns
339                        .pattern
340                        .iter()
341                        .map(|dp| DependencyPattern::from(dp.clone())),
342                );
343
344                Self::DependencyType(list)
345            }
346            PluginTypeDescriptorEnum::PluginType(pt) => Self::PluginType(pt.name),
347        }
348    }
349}
350impl From<spec::types::PluginTypeDescriptor> for PluginTypeDescriptorEnum {
351    fn from(ptd: spec::types::PluginTypeDescriptor) -> Self {
352        Self::from(ptd.value)
353    }
354}
355
356#[derive(Clone, Debug, PartialEq, Eq)]
357pub struct DependencyPattern {
358    pub dependencies: Dependency,
359    pub typ: PluginTypeEnum,
360}
361impl From<spec::types::DependencyPattern> for DependencyPattern {
362    fn from(dp: spec::types::DependencyPattern) -> Self {
363        Self {
364            dependencies: Dependency::from(dp.dependencies),
365            typ: dp.typ.name,
366        }
367    }
368}
369
370#[derive(Clone, Debug, PartialEq, Eq)]
371pub struct ConditionalInstallPattern {
372    pub dependencies: Dependency,
373    pub files: Vec<FileType>,
374}
375impl From<crate::spec::types::ConditionalInstallPattern> for ConditionalInstallPattern {
376    fn from(spec: crate::spec::types::ConditionalInstallPattern) -> Self {
377        Self {
378            dependencies: Dependency::from(spec.dependencies),
379            files: spec.files.list.unwrap_or_default(),
380        }
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use crate::spec::Config as SpecConfig;
387    use crate::{Config, Info};
388
389    #[test]
390    pub fn info() {
391        let xml = r#"
392        <?xml version="1.0"?>
393        <fomod xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
394          <Name>StarUI Inventory</Name>
395          <Version>2.1</Version>
396          <Author>m8r98a4f2</Author>
397          <Website>https://www.nexusmods.com/starfield/mods/773</Website>
398          <CategoryId>37</CategoryId>
399        </fomod>
400       "#;
401
402        let info: Info = quick_xml::de::from_str(&xml).unwrap();
403        assert_eq!(info.name, Some("StarUI Inventory".to_string()));
404        assert_eq!(info.version, Some("2.1".to_string()));
405        assert_eq!(info.author, Some("m8r98a4f2".to_string()));
406        assert_eq!(
407            info.website,
408            Some("https://www.nexusmods.com/starfield/mods/773".to_string())
409        );
410        assert_eq!(info.category_id, Some(37));
411    }
412
413    #[test]
414    pub fn required_files() {
415        let xml = r#"
416        <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
417            xsi:noNamespaceSchemaLocation="http://qconsulting.ca/fo3/ModConfig5.0.xsd">
418
419            <moduleName>Example Mod</moduleName>
420
421            <requiredInstallFiles>
422                <file source="example.plugin"/>
423                <file source="example2.plugin"/>
424            </requiredInstallFiles>
425        </config>
426        "#;
427
428        let config: SpecConfig = quick_xml::de::from_str(&xml).unwrap();
429        assert_eq!(config.module_name, "Example Mod".to_string());
430
431        let file_list = config
432            .required_install_files
433            .as_ref()
434            .unwrap()
435            .list
436            .as_ref()
437            .unwrap();
438        assert_eq!(file_list.len(), 2);
439        assert_eq!(file_list[0].source, "example.plugin");
440        assert_eq!(file_list[1].source, "example2.plugin");
441
442        let config = Config::from(config);
443    }
444
445    #[test]
446    pub fn module_deps() {
447        let xml = r#"
448        <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
449            xsi:noNamespaceSchemaLocation="http://qconsulting.ca/fo3/ModConfig5.0.xsd">
450
451            <moduleName>Example Mod</moduleName>
452
453            <moduleDependencies operator="And">
454                <fileDependency file="depend1.plugin" state="Active"/>
455            </moduleDependencies>
456
457            <requiredInstallFiles>
458                <file source="example.plugin"/>
459            </requiredInstallFiles>
460        </config>
461        "#;
462
463        let config: SpecConfig = quick_xml::de::from_str(&xml).unwrap();
464
465        let config = Config::from(config);
466    }
467
468    #[test]
469    pub fn module_deps2() {
470        let xml = r#"
471        <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
472            xsi:noNamespaceSchemaLocation="http://qconsulting.ca/fo3/ModConfig5.0.xsd">
473
474            <moduleName>Example Mod</moduleName>
475
476            <moduleDependencies operator="And">
477                <fileDependency file="depend1.plugin" state="Active"/>
478                <dependencies operator="Or">
479                    <fileDependency file="depend2v1.plugin" state="Active"/>
480                    <fileDependency file="depend2v2.plugin" state="Active"/>
481                </dependencies>
482            </moduleDependencies>
483
484            <requiredInstallFiles>
485                <file source="example.plugin"/>
486            </requiredInstallFiles>
487
488        </config>
489        "#;
490
491        let config: SpecConfig = quick_xml::de::from_str(&xml).unwrap();
492
493        let config = Config::from(config);
494    }
495
496    #[test]
497    pub fn install_steps() {
498        let xml = r#"
499        <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
500            xsi:noNamespaceSchemaLocation="http://qconsulting.ca/fo3/ModConfig5.0.xsd">
501
502            <moduleName>Example Mod</moduleName>
503
504            <moduleDependencies operator="And">
505                <fileDependency file="depend1.plugin" state="Active"/>
506                <dependencies operator="Or">
507                    <fileDependency file="depend2v1.plugin" state="Active"/>
508                    <fileDependency file="depend2v2.plugin" state="Active"/>
509                </dependencies>
510            </moduleDependencies>
511
512            <installSteps order="Explicit">
513                <installStep name="Choose Option">
514                    <optionalFileGroups order="Explicit">
515                        <group name="Select an option:" type="SelectExactlyOne">
516                            <plugins order="Explicit">
517                                <plugin name="Option A">
518                                    <description>Select this to install Option A!</description>
519                                    <image path="fomod/option_a.png"/>
520                                    <files>
521                                        <folder source="option_a"/>
522                                    </files>
523                                    <typeDescriptor>
524                                        <type name="Recommended"/>
525                                    </typeDescriptor>
526                                </plugin>
527                                <plugin name="Option B">
528                                    <description>Select this to install Option B!</description>
529                                    <image path="fomod/option_b.png"/>
530            						<files />
531                                    <typeDescriptor>
532                                        <type name="Optional"/>
533                                    </typeDescriptor>
534                                </plugin>
535                            </plugins>
536                        </group>
537                    </optionalFileGroups>
538                </installStep>
539            </installSteps>
540
541        </config>
542        "#;
543
544        let config: SpecConfig = quick_xml::de::from_str(&xml).unwrap();
545
546        let config = Config::from(config);
547    }
548
549    #[test]
550    pub fn install_matrix() {
551        let xml = r#"
552        <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
553            xsi:noNamespaceSchemaLocation="http://qconsulting.ca/fo3/ModConfig5.0.xsd">
554
555            <moduleName>Example Mod</moduleName>
556
557            <moduleDependencies operator="And">
558                <fileDependency file="depend1.plugin" state="Active"/>
559                <dependencies operator="Or">
560                    <fileDependency file="depend2v1.plugin" state="Active"/>
561                    <fileDependency file="depend2v2.plugin" state="Active"/>
562                </dependencies>
563            </moduleDependencies>
564
565            <installSteps order="Explicit">
566                <installStep name="Choose Option">
567                    <optionalFileGroups order="Explicit">
568
569                        <group name="Select an option:" type="SelectExactlyOne">
570                            <plugins order="Explicit">
571
572                                <plugin name="Option A">
573                                    <description>Select this to install Option A!</description>
574                                    <image path="fomod/option_a.png"/>
575                                    <conditionFlags>
576                                        <flag name="option_a">selected</flag>
577                                    </conditionFlags>
578                                    <typeDescriptor>
579                                        <type name="Recommended"/>
580                                    </typeDescriptor>
581                                </plugin>
582
583                                <plugin name="Option B">
584                                    <description>Select this to install Option B!</description>
585                                    <image path="fomod/option_b.png"/>
586                                    <conditionFlags>
587                                        <flag name="option_b">selected</flag>
588                                    </conditionFlags>
589                                    <typeDescriptor>
590                                        <type name="Optional"/>
591                                    </typeDescriptor>
592                                </plugin>
593
594                            </plugins>
595                        </group>
596
597                        <group name="Select a texture:" type="SelectExactlyOne">
598                            <plugins order="Explicit">
599
600                                <plugin name="Texture Blue">
601                                    <description>Select this to install Texture Blue!</description>
602                                    <image path="fomod/texture_blue.png"/>
603                                    <conditionFlags>
604                                        <flag name="texture_blue">selected</flag>
605                                    </conditionFlags>
606                                    <typeDescriptor>
607                                        <type name="Optional"/>
608                                    </typeDescriptor>
609                                </plugin>
610
611                                <plugin name="Texture Red">
612                                    <description>Select this to install Texture Red!</description>
613                                    <image path="fomod/texture_red.png"/>
614                                    <conditionFlags>
615                                        <flag name="texture_red">selected</flag>
616                                    </conditionFlags>
617                                    <typeDescriptor>
618                                        <type name="Optional"/>
619                                    </typeDescriptor>
620                                </plugin>
621
622                            </plugins>
623                        </group>
624
625                    </optionalFileGroups>
626                </installStep>
627            </installSteps>
628
629            <conditionalFileInstalls>
630                <patterns>
631                    <pattern>
632                        <dependencies operator="And">
633                            <flagDependency flag="option_a" value="selected"/>
634                            <flagDependency flag="texture_blue" value="selected"/>
635                        </dependencies>
636                        <files>
637                            <folder source="option_a"/>
638                            <folder source="texture_blue_a"/>
639                        </files>
640                    </pattern>
641                    <pattern>
642                        <dependencies operator="And">
643                            <flagDependency flag="option_a" value="selected"/>
644                            <flagDependency flag="texture_red" value="selected"/>
645                        </dependencies>
646                        <files>
647                            <folder source="option_a"/>
648                            <folder source="texture_red_a"/>
649                        </files>
650                    </pattern>
651                    <pattern>
652                        <dependencies operator="And">
653                            <flagDependency flag="option_b" value="selected"/>
654                            <flagDependency flag="texture_blue" value="selected"/>
655                        </dependencies>
656                        <files>
657                            <folder source="option_b"/>
658                            <folder source="texture_blue_b"/>
659                        </files>
660                    </pattern>
661                    <pattern>
662                        <dependencies operator="And">
663                            <flagDependency flag="option_b" value="selected"/>
664                            <flagDependency flag="texture_red" value="selected"/>
665                        </dependencies>
666                        <files>
667                            <folder source="option_b"/>
668                            <folder source="texture_red_b"/>
669                        </files>
670                    </pattern>
671                </patterns>
672            </conditionalFileInstalls>
673
674        </config>
675        "#;
676
677        let config: SpecConfig = quick_xml::de::from_str(&xml).unwrap();
678
679        let config = Config::from(config);
680    }
681}