aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src/core/modpack/resolver.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src-tauri/src/core/modpack/resolver.rs')
-rw-r--r--src-tauri/src/core/modpack/resolver.rs177
1 files changed, 177 insertions, 0 deletions
diff --git a/src-tauri/src/core/modpack/resolver.rs b/src-tauri/src/core/modpack/resolver.rs
new file mode 100644
index 0000000..6f5f4aa
--- /dev/null
+++ b/src-tauri/src/core/modpack/resolver.rs
@@ -0,0 +1,177 @@
+use std::collections::{HashMap, HashSet};
+
+use futures::future::BoxFuture;
+
+use super::{
+ curseforge::{
+ CurseForgeApi, CurseForgeFile, CurseForgeGetModFilesRequestBody,
+ CurseForgeGetModsByIdsListRequestBody, CurseForgeMod,
+ },
+ types::{ModpackFile, ParsedModpack},
+};
+
+const CURSEFORGE_RESOURCE_PACK_CLASS_ID: u64 = 12;
+const CURSEFORGE_SHADER_PACK_CLASS_ID: u64 = 6552;
+
+pub(crate) trait ModpackFileResolver: Send + Sync {
+ fn resolve<'a>(
+ &'a self,
+ modpack: ParsedModpack,
+ ) -> BoxFuture<'a, Result<ParsedModpack, String>>;
+}
+
+pub(crate) struct ResolverChain {
+ resolvers: Vec<Box<dyn ModpackFileResolver>>,
+}
+
+impl ResolverChain {
+ pub(crate) fn new(resolvers: Vec<Box<dyn ModpackFileResolver>>) -> Self {
+ Self { resolvers }
+ }
+
+ pub(crate) fn push<R>(&mut self, resolver: R)
+ where
+ R: ModpackFileResolver + 'static,
+ {
+ self.resolvers.push(Box::new(resolver));
+ }
+}
+
+impl Default for ResolverChain {
+ fn default() -> Self {
+ Self::new(vec![Box::new(CurseForgeFileResolver::default())])
+ }
+}
+
+impl ModpackFileResolver for ResolverChain {
+ fn resolve<'a>(
+ &'a self,
+ mut modpack: ParsedModpack,
+ ) -> BoxFuture<'a, Result<ParsedModpack, String>> {
+ Box::pin(async move {
+ for resolver in &self.resolvers {
+ modpack = resolver.resolve(modpack).await?;
+ }
+ Ok(modpack)
+ })
+ }
+}
+
+pub(crate) struct CurseForgeFileResolver {
+ api: CurseForgeApi,
+}
+
+impl CurseForgeFileResolver {
+ pub(crate) fn new(api: CurseForgeApi) -> Self {
+ Self { api }
+ }
+
+ async fn resolve_files(&self, files: &[ModpackFile]) -> Result<Vec<ModpackFile>, String> {
+ let file_ids: Vec<u64> = files.iter().filter_map(file_id).collect();
+ if file_ids.is_empty() {
+ return Ok(Vec::new());
+ }
+
+ let file_items = self
+ .api
+ .get_files(&CurseForgeGetModFilesRequestBody::new(file_ids))
+ .await?
+ .data;
+ let mod_ids: Vec<u64> = file_items
+ .iter()
+ .map(|item| item.mod_id)
+ .collect::<HashSet<_>>()
+ .into_iter()
+ .collect();
+ let class_ids = self.class_ids(&mod_ids).await;
+
+ Ok(file_items
+ .into_iter()
+ .map(|item| {
+ let class_id = class_ids.get(&item.mod_id).copied();
+ map_curseforge_file(item, class_id)
+ })
+ .collect())
+ }
+
+ async fn class_ids(&self, mod_ids: &[u64]) -> HashMap<u64, u64> {
+ let Ok(mods) = self
+ .api
+ .get_mods(&CurseForgeGetModsByIdsListRequestBody::new(
+ mod_ids.to_vec(),
+ ))
+ .await
+ .map(|response| response.data)
+ else {
+ return HashMap::new();
+ };
+
+ mods.into_iter().filter_map(mod_class_entry).collect()
+ }
+}
+
+impl Default for CurseForgeFileResolver {
+ fn default() -> Self {
+ Self::new(CurseForgeApi::default())
+ }
+}
+
+impl ModpackFileResolver for CurseForgeFileResolver {
+ fn resolve<'a>(
+ &'a self,
+ mut modpack: ParsedModpack,
+ ) -> BoxFuture<'a, Result<ParsedModpack, String>> {
+ Box::pin(async move {
+ if modpack.info.modpack_type != "curseforge" {
+ return Ok(modpack);
+ }
+
+ let files = self.resolve_files(&modpack.files).await?;
+ modpack.files = files;
+ Ok(modpack)
+ })
+ }
+}
+
+fn file_id(file: &ModpackFile) -> Option<u64> {
+ file.url
+ .strip_prefix("curseforge://")?
+ .split(':')
+ .nth(1)?
+ .parse()
+ .ok()
+}
+
+fn map_curseforge_file(file: CurseForgeFile, class_id: Option<u64>) -> ModpackFile {
+ let CurseForgeFile {
+ id,
+ file_name,
+ download_url,
+ file_length,
+ ..
+ } = file;
+ let url = download_url.unwrap_or_else(|| {
+ format!(
+ "https://edge.forgecdn.net/files/{}/{}/{}",
+ id / 1000,
+ id % 1000,
+ file_name
+ )
+ });
+ let path = match class_id {
+ Some(CURSEFORGE_RESOURCE_PACK_CLASS_ID) => format!("resourcepacks/{file_name}"),
+ Some(CURSEFORGE_SHADER_PACK_CLASS_ID) => format!("shaderpacks/{file_name}"),
+ _ => format!("mods/{file_name}"),
+ };
+
+ ModpackFile {
+ url,
+ path,
+ size: Some(file_length),
+ sha1: None,
+ }
+}
+
+fn mod_class_entry(item: CurseForgeMod) -> Option<(u64, u64)> {
+ Some((item.id, item.class_id?))
+}