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 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 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 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}