You've already forked AstralRinth
* working on amcros * fleshed out draft * added feature support * finished loading * Fixed issue with multiple data types in macro * Working, and added more loading uses * added window scopes * clippy, fmt * working other variants * fmt; clippy * prettier * refactored emissions to use increment * fixed deadlock * doc changes * clippy, prettier * uuid change * restructured events to util * loading restructure * merge fixes * comments mistake * better cfg tauri feature structuring * added extra fields to some loading enum variants * removed Option<> * added pack + version labels * doc change
359 lines
11 KiB
Rust
359 lines
11 KiB
Rust
use crate::config::MODRINTH_API_URL;
|
|
use crate::data::ModLoader;
|
|
use crate::event::emit::{init_loading, loading_try_for_each_concurrent};
|
|
use crate::event::LoadingBarType;
|
|
use crate::state::{ModrinthProject, ModrinthVersion, SideType};
|
|
use crate::util::fetch::{
|
|
fetch, fetch_json, fetch_mirrors, write, write_cached_icon,
|
|
};
|
|
use crate::State;
|
|
use async_zip::tokio::read::seek::ZipFileReader;
|
|
use reqwest::Method;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::io::Cursor;
|
|
use std::path::{Component, PathBuf};
|
|
use tokio::fs;
|
|
|
|
#[derive(Serialize, Deserialize, Eq, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct PackFormat {
|
|
pub game: String,
|
|
pub format_version: i32,
|
|
pub version_id: String,
|
|
pub name: String,
|
|
pub summary: Option<String>,
|
|
pub files: Vec<PackFile>,
|
|
pub dependencies: HashMap<PackDependency, String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Eq, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct PackFile {
|
|
pub path: String,
|
|
pub hashes: HashMap<PackFileHash, String>,
|
|
pub env: Option<HashMap<EnvType, SideType>>,
|
|
pub downloads: Vec<String>,
|
|
pub file_size: u32,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
|
|
#[serde(rename_all = "camelCase", from = "String")]
|
|
enum PackFileHash {
|
|
Sha1,
|
|
Sha512,
|
|
Unknown(String),
|
|
}
|
|
|
|
impl From<String> for PackFileHash {
|
|
fn from(s: String) -> Self {
|
|
return match s.as_str() {
|
|
"sha1" => PackFileHash::Sha1,
|
|
"sha512" => PackFileHash::Sha512,
|
|
_ => PackFileHash::Unknown(s),
|
|
};
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
|
|
#[serde(rename_all = "camelCase")]
|
|
enum EnvType {
|
|
Client,
|
|
Server,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
enum PackDependency {
|
|
Forge,
|
|
FabricLoader,
|
|
QuiltLoader,
|
|
Minecraft,
|
|
}
|
|
|
|
pub async fn install_pack_from_version_id(
|
|
version_id: String,
|
|
) -> crate::Result<PathBuf> {
|
|
let state = State::get().await?;
|
|
|
|
let version: ModrinthVersion = fetch_json(
|
|
Method::GET,
|
|
&format!("{}version/{}", MODRINTH_API_URL, version_id),
|
|
None,
|
|
None,
|
|
&state.io_semaphore,
|
|
)
|
|
.await?;
|
|
|
|
let (url, hash) =
|
|
if let Some(file) = version.files.iter().find(|x| x.primary) {
|
|
Some((file.url.clone(), file.hashes.get("sha1")))
|
|
} else {
|
|
version
|
|
.files
|
|
.first()
|
|
.map(|file| (file.url.clone(), file.hashes.get("sha1")))
|
|
}
|
|
.ok_or_else(|| {
|
|
crate::ErrorKind::InputError(
|
|
"Specified version has no files".to_string(),
|
|
)
|
|
})?;
|
|
|
|
let file = fetch(&url, hash.map(|x| &**x), &state.io_semaphore).await?;
|
|
|
|
let project: ModrinthProject = fetch_json(
|
|
Method::GET,
|
|
&format!("{}project/{}", MODRINTH_API_URL, version.project_id),
|
|
None,
|
|
None,
|
|
&state.io_semaphore,
|
|
)
|
|
.await?;
|
|
|
|
let icon = if let Some(icon_url) = project.icon_url {
|
|
let state = State::get().await?;
|
|
let icon_bytes = fetch(&icon_url, None, &state.io_semaphore).await?;
|
|
|
|
let filename = icon_url.rsplit('/').next();
|
|
|
|
if let Some(filename) = filename {
|
|
Some(
|
|
write_cached_icon(
|
|
filename,
|
|
&state.directories.caches_dir(),
|
|
icon_bytes,
|
|
&state.io_semaphore,
|
|
)
|
|
.await?,
|
|
)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
install_pack(file, icon, Some(version.project_id), Some(version.id)).await
|
|
}
|
|
|
|
pub async fn install_pack_from_file(path: PathBuf) -> crate::Result<PathBuf> {
|
|
let file = fs::read(path).await?;
|
|
|
|
install_pack(bytes::Bytes::from(file), None, None, None).await
|
|
}
|
|
|
|
async fn install_pack(
|
|
file: bytes::Bytes,
|
|
icon: Option<PathBuf>,
|
|
project_id: Option<String>,
|
|
version_id: Option<String>,
|
|
) -> crate::Result<PathBuf> {
|
|
let state = &State::get().await?;
|
|
|
|
let reader = Cursor::new(&file);
|
|
|
|
// Create zip reader around file
|
|
let mut zip_reader = ZipFileReader::new(reader).await.map_err(|_| {
|
|
crate::Error::from(crate::ErrorKind::InputError(
|
|
"Failed to read input modpack zip".to_string(),
|
|
))
|
|
})?;
|
|
|
|
// Extract index of modrinth.index.json
|
|
let zip_index_option = zip_reader
|
|
.file()
|
|
.entries()
|
|
.iter()
|
|
.position(|f| f.entry().filename() == "modrinth.index.json");
|
|
if let Some(zip_index) = zip_index_option {
|
|
let mut manifest = String::new();
|
|
let entry = zip_reader
|
|
.file()
|
|
.entries()
|
|
.get(zip_index)
|
|
.unwrap()
|
|
.entry()
|
|
.clone();
|
|
let mut reader = zip_reader.entry(zip_index).await?;
|
|
reader.read_to_string_checked(&mut manifest, &entry).await?;
|
|
|
|
let pack: PackFormat = serde_json::from_str(&manifest)?;
|
|
|
|
if &*pack.game != "minecraft" {
|
|
return Err(crate::ErrorKind::InputError(
|
|
"Pack does not support Minecraft".to_string(),
|
|
)
|
|
.into());
|
|
}
|
|
|
|
let mut game_version = None;
|
|
let mut mod_loader = None;
|
|
let mut loader_version = None;
|
|
for (key, value) in pack.dependencies {
|
|
match key {
|
|
PackDependency::Forge => {
|
|
mod_loader = Some(ModLoader::Forge);
|
|
loader_version = Some(value);
|
|
}
|
|
PackDependency::FabricLoader => {
|
|
mod_loader = Some(ModLoader::Fabric);
|
|
loader_version = Some(value);
|
|
}
|
|
PackDependency::QuiltLoader => {
|
|
mod_loader = Some(ModLoader::Quilt);
|
|
loader_version = Some(value);
|
|
}
|
|
PackDependency::Minecraft => game_version = Some(value),
|
|
}
|
|
}
|
|
|
|
let game_version = if let Some(game_version) = game_version {
|
|
game_version
|
|
} else {
|
|
return Err(crate::ErrorKind::InputError(
|
|
"Pack did not specify Minecraft version".to_string(),
|
|
)
|
|
.into());
|
|
};
|
|
|
|
let pack_name = pack.name.clone();
|
|
let profile = crate::api::profile_create::profile_create(
|
|
pack.name,
|
|
game_version.clone(),
|
|
mod_loader.unwrap_or(ModLoader::Vanilla),
|
|
loader_version,
|
|
icon,
|
|
project_id.clone(),
|
|
)
|
|
.await?;
|
|
|
|
let loading_bar = init_loading(
|
|
LoadingBarType::PackDownload {
|
|
pack_name ,
|
|
pack_id: project_id,
|
|
pack_version: version_id,
|
|
},
|
|
100.0,
|
|
"Downloading modpack...",
|
|
)
|
|
.await?;
|
|
let num_files = pack.files.len();
|
|
use futures::StreamExt;
|
|
loading_try_for_each_concurrent(
|
|
futures::stream::iter(pack.files.into_iter())
|
|
.map(Ok::<PackFile, crate::Error>),
|
|
None,
|
|
Some(&loading_bar),
|
|
100.0,
|
|
num_files,
|
|
None,
|
|
|project| {
|
|
let profile = profile.clone();
|
|
async move {
|
|
//TODO: Future update: prompt user for optional files in a modpack
|
|
if let Some(env) = project.env {
|
|
if env
|
|
.get(&EnvType::Client)
|
|
.map(|x| x == &SideType::Unsupported)
|
|
.unwrap_or(false)
|
|
{
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
let file = fetch_mirrors(
|
|
&project
|
|
.downloads
|
|
.iter()
|
|
.map(|x| &**x)
|
|
.collect::<Vec<&str>>(),
|
|
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
|
|
&state.io_semaphore,
|
|
)
|
|
.await?;
|
|
|
|
let path =
|
|
std::path::Path::new(&project.path).components().next();
|
|
if let Some(path) = path {
|
|
match path {
|
|
Component::CurDir | Component::Normal(_) => {
|
|
let path = profile.join(project.path);
|
|
write(&path, &file, &state.io_semaphore)
|
|
.await?;
|
|
}
|
|
_ => {}
|
|
};
|
|
}
|
|
Ok(())
|
|
}
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
let extract_overrides = |overrides: String| async {
|
|
let reader = Cursor::new(&file);
|
|
|
|
let mut overrides_zip =
|
|
ZipFileReader::new(reader).await.map_err(|_| {
|
|
crate::Error::from(crate::ErrorKind::InputError(
|
|
"Failed extract overrides Zip".to_string(),
|
|
))
|
|
})?;
|
|
|
|
let profile = profile.clone();
|
|
async move {
|
|
for index in 0..overrides_zip.file().entries().len() {
|
|
let file = overrides_zip
|
|
.file()
|
|
.entries()
|
|
.get(index)
|
|
.unwrap()
|
|
.entry()
|
|
.clone();
|
|
|
|
let file_path = PathBuf::from(file.filename());
|
|
if file.filename().starts_with(&overrides)
|
|
&& !file.filename().ends_with('/')
|
|
{
|
|
// Reads the file into the 'content' variable
|
|
let mut content = Vec::new();
|
|
let mut reader = overrides_zip.entry(index).await?;
|
|
reader.read_to_end_checked(&mut content, &file).await?;
|
|
|
|
let mut new_path = PathBuf::new();
|
|
let components = file_path.components().skip(1);
|
|
|
|
for component in components {
|
|
new_path.push(component);
|
|
}
|
|
|
|
if new_path.file_name().is_some() {
|
|
write(
|
|
&profile.join(new_path),
|
|
&content,
|
|
&state.io_semaphore,
|
|
)
|
|
.await?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok::<(), crate::Error>(())
|
|
}
|
|
.await
|
|
};
|
|
|
|
extract_overrides("overrides".to_string()).await?;
|
|
extract_overrides("client_overrides".to_string()).await?;
|
|
|
|
super::profile::sync(&profile).await?;
|
|
|
|
Ok(profile)
|
|
} else {
|
|
Err(crate::Error::from(crate::ErrorKind::InputError(
|
|
"No pack manifest found in mrpack".to_string(),
|
|
)))
|
|
}
|
|
}
|