Skip to main content

conduit_cli/core/engine/resolver/
addon.rs

1use crate::core::engine::resolver::Resolver;
2use crate::core::domain::addon::AddonType;
3use crate::core::domain::loader::Loader;
4use crate::core::domain::source::{AddonSource, Hash, SourceType};
5use crate::errors::{ConduitError, ConduitResult};
6use std::collections::{HashMap, HashSet};
7
8pub struct ResolvedAddon {
9    pub id: String,
10    pub slug: String,
11    pub file_name: String,
12    pub r#type: AddonType,
13    pub loaders: Vec<Loader>,
14    pub download_url: String,
15    pub source: AddonSource,
16    pub dependencies: Vec<String>,
17}
18
19impl Resolver {
20    pub async fn resolve_recursively(
21        &self,
22        identifier: &str,
23        mc_version: &str,
24        loader: &Loader,
25        expected_type: AddonType,
26    ) -> ConduitResult<Vec<ResolvedAddon>> {
27        let mut resolved_map: HashMap<String, ResolvedAddon> = HashMap::new();
28        let mut seen_ids: HashSet<String> = HashSet::new();
29        let mut to_resolve = vec![identifier.to_string()];
30
31        while let Some(current_id) = to_resolve.pop() {
32            let resolved = self
33                .resolve_modrinth_addon(&current_id, mc_version, loader, expected_type.clone())
34                .await?;
35
36            if seen_ids.contains(&resolved.id) {
37                continue;
38            }
39
40            let project_id = resolved.id.clone();
41
42            for dep_id in &resolved.dependencies {
43                if !seen_ids.contains(dep_id) {
44                    to_resolve.push(dep_id.clone());
45                }
46            }
47
48            seen_ids.insert(project_id.clone());
49            resolved_map.insert(project_id, resolved);
50        }
51
52        Ok(resolved_map.into_values().collect())
53    }
54
55    pub async fn resolve_modrinth_addon(
56        &self,
57        id_or_slug: &str,
58        mc_version: &str,
59        loader: &Loader,
60        expected_type: AddonType,
61    ) -> ConduitResult<ResolvedAddon> {
62        let project = self.api.modrinth.get_project(id_or_slug).await?;
63
64        let mut loaders = vec![
65            match loader {
66                Loader::Vanilla => "minecraft",
67                Loader::Fabric => "fabric",
68                Loader::Forge { .. } => "forge",
69                Loader::Neoforge { .. } => "neoforge",
70                Loader::Paper => "paper",
71                Loader::Purpur => "purpur",
72            }
73            .to_string(),
74        ];
75
76        if expected_type == AddonType::Datapack {
77            loaders.push("datapack".to_string());
78        }
79
80        let versions = self
81            .api
82            .modrinth
83            .get_project_versions(&project.id, &loaders, &[mc_version.to_string()])
84            .await?;
85
86        let version = versions
87            .into_iter()
88            .find(|v| {
89                let is_datapack = v.loaders.contains(&"datapack".to_string());
90
91                match expected_type {
92                    AddonType::Datapack => is_datapack,
93                    AddonType::Plugin => {
94                        v.loaders.contains(&"paper".to_string())
95                            || v.loaders.contains(&"spigot".to_string())
96                    }
97                    AddonType::Mod => !is_datapack,
98                }
99            })
100            .ok_or_else(|| {
101                ConduitError::NotFound(format!(
102                    "No compatible {expected_type:?} found for {id_or_slug} on {mc_version}"
103                ))
104            })?;
105
106        let file = version
107            .files
108            .iter()
109            .find(|f| f.primary)
110            .or_else(|| version.files.first())
111            .ok_or_else(|| ConduitError::NotFound(format!("No files found for {id_or_slug}")))?;
112
113        Ok(ResolvedAddon {
114            id: project.id.clone(),
115            slug: project.slug.clone(),
116            file_name: file.filename.clone(),
117            r#type: expected_type,
118            loaders: vec![loader.clone()],
119            download_url: file.url.clone(),
120            source: AddonSource {
121                r#type: SourceType::Modrinth {
122                    id: project.id,
123                    slug: project.slug,
124                },
125                hash: Hash {
126                    sha1: Some(file.hashes.sha1.clone()),
127                    sha256: None,
128                    sha512: Some(file.hashes.sha512.clone()),
129                },
130            },
131            dependencies: version
132                .dependencies
133                .into_iter()
134                .filter(|dep| dep.dependency_type == "required")
135                .filter_map(|dep| dep.project_id)
136                .collect(),
137        })
138    }
139}