Added modpack creation and file adding

This commit is contained in:
Daniel Hutzley 2021-12-05 10:41:56 -08:00
parent fe3581756f
commit 5ffddd6c8a
6 changed files with 339 additions and 25 deletions

12
Cargo.lock generated
View File

@ -46,6 +46,17 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38de00daab4eac7d753e97697066238d67ce9d7e2d823ab4f72fe14af29f3f33"
[[package]]
name = "async-trait"
version = "0.1.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.0.1"
@ -1022,6 +1033,7 @@ name = "theseus"
version = "0.1.0"
dependencies = [
"argh",
"async-trait",
"bytes",
"chrono",
"daedalus",

View File

@ -8,6 +8,7 @@ edition = "2018"
[dependencies]
thiserror = "1.0"
async-trait = "0.1.51"
daedalus = "0.1.6"

View File

@ -1,3 +1,4 @@
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::convert::TryFrom;
@ -11,7 +12,7 @@ use serde::{Deserialize, Serialize};
pub const DEFAULT_FORMAT_VERSION: u32 = 1;
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Manifest<'a> {
pub format_version: u32,
@ -32,7 +33,7 @@ impl TryFrom<Manifest<'_>> for pack::Modpack {
.files
.into_iter()
.map(pack::ModpackFile::try_from)
.collect::<ModpackResult<Vec<pack::ModpackFile>>>()?;
.collect::<ModpackResult<HashSet<pack::ModpackFile>>>()?;
Ok(Self {
name: String::from(manifest.name),
@ -60,7 +61,7 @@ fn get_loader_version(loader: ModLoader, version: &str) -> ModpackResult<String>
.iter()
.find(|&it| it.id == version)
.ok_or(ModpackError::VersionError(format!(
"No versions of {:?} exist for Minecraft {}",
"No versions of modloader {:?} exist for Minecraft {}",
loader, version
)))?
.loaders[&LoaderType::Latest]
@ -94,7 +95,7 @@ impl<'a> TryFrom<&'a pack::Modpack> for Manifest<'a> {
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ManifestFile<'a> {
#[serde(borrow)]
@ -134,7 +135,7 @@ impl<'a> From<&'a pack::ModpackFile> for ManifestFile<'a> {
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
pub struct ManifestHashes<'a> {
pub sha1: &'a str,
}
@ -153,7 +154,7 @@ impl<'a> From<&'a pack::ModpackFileHashes> for ManifestHashes<'a> {
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
pub struct ManifestEnvs {
pub client: ManifestEnv,
pub server: ManifestEnv,
@ -208,7 +209,7 @@ impl From<pack::ModpackEnv> for ManifestEnvs {
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
// HACK: I've tried for hours to get this working zero-copy, but I'm beat. If someone else wants to
// go through the #<!! of implementing it, be my guest.

View File

@ -11,7 +11,8 @@ use zip::ZipArchive;
use self::{manifest::Manifest, pack::Modpack};
pub mod pack;
mod manifest;
pub mod manifest;
pub mod modrinth_api;
pub const MANIFEST_PATH: &'static str = "index.json";
pub const OVERRIDES_PATH: &'static str = "overrides/";
@ -45,8 +46,11 @@ pub enum ModpackError {
#[error("Error joining futures: {0}")]
JoinError(#[from] tokio::task::JoinError),
#[error("Error fetching modloader version: {0}")]
#[error("Versioning Error: {0}")]
VersionError(String),
#[error("Error downloading file: {0}")]
FetchError(#[from] reqwest::Error)
}
type ModpackResult<T> = Result<T, ModpackError>;

View File

@ -0,0 +1,176 @@
use std::{
collections::HashSet,
convert::TryFrom,
path::{Path, PathBuf},
};
use crate::launcher::ModLoader;
use super::{
manifest::{ManifestEnv, ManifestEnvs, ManifestHashes},
pack::{ModpackEnv, ModpackFile, ModpackFileHashes, ModpackGame},
ModpackError, ModpackResult,
};
use async_trait::async_trait;
use bytes::Bytes;
use futures::future::try_join_all;
use serde::Deserialize;
use tokio::try_join;
#[async_trait]
pub trait ModrinthAPI {
async fn get_latest_version(
&self,
project: &str,
channel: &str,
game: &ModpackGame,
) -> ModpackResult<HashSet<ModpackFile>>;
async fn get_version(&self, version: &str) -> ModpackResult<HashSet<ModpackFile>>;
}
#[derive(Debug)]
pub struct ModrinthV1(pub String);
#[derive(Debug, Deserialize)]
struct ModrinthV1Project<'a> {
title: &'a str,
client_side: &'a str,
server_side: &'a str,
}
#[derive(Debug, Deserialize)]
struct ModrinthV1ProjectVersion<'a> {
#[serde(borrow)]
dependencies: HashSet<&'a str>,
#[serde(borrow)]
game_versions: HashSet<&'a str>,
version_type: &'a str,
files: Vec<ModrinthV1ProjectVersionFile<'a>>,
#[serde(borrow)]
loaders: HashSet<&'a str>,
}
#[derive(Clone, Debug, Deserialize)]
struct ModrinthV1ProjectVersionFile<'a> {
hashes: ManifestHashes<'a>,
url: &'a str,
filename: &'a str,
}
impl From<ModrinthV1ProjectVersionFile<'_>> for ModpackFile {
fn from(file: ModrinthV1ProjectVersionFile<'_>) -> Self {
Self {
hashes: ModpackFileHashes::from(file.hashes),
downloads: {
let mut downloads: HashSet<String> = HashSet::new();
downloads.insert(String::from(file.url));
downloads
},
path: PathBuf::from(file.filename),
// WARNING: Since the sidedness of version 1 API requests is unknown, the environemnt is
// set here as both.
env: ModpackEnv::Both,
}
}
}
#[async_trait]
impl ModrinthAPI for ModrinthV1 {
async fn get_latest_version(
&self,
project: &str,
channel: &str,
game: &ModpackGame,
) -> ModpackResult<HashSet<ModpackFile>> {
// Fetch metadata
let (project_json, versions_json): (Bytes, Bytes) = try_join!(
try_get_json(format!("{}/api/v1/mod/{}", self.0, project)),
try_get_json(format!("{}/api/v1/mod/{}/version", self.0, project)),
)?;
let (mut project_deserializer, mut versions_deserializer) = (
serde_json::Deserializer::from_slice(&project_json),
serde_json::Deserializer::from_slice(&versions_json),
);
let (project, versions) = (
ModrinthV1Project::deserialize(&mut project_deserializer)?,
Vec::deserialize(&mut versions_deserializer)?,
);
let (game_version, loader) = match game {
ModpackGame::Minecraft(_, ModLoader::Vanilla) => Err(ModpackError::VersionError(
String::from("Modrinth V1 does not support vanilla projects"),
)),
ModpackGame::Minecraft(ref version, ref loader) => Ok((version, loader)),
_ => Err(ModpackError::VersionError(String::from(
"Attempted to use Modrinth API V1 to install a non-Minecraft project!",
))),
}?;
let version: ModrinthV1ProjectVersion = versions
.into_iter()
.find(|it: &ModrinthV1ProjectVersion| {
let loader_str = match loader {
ModLoader::Fabric => "fabric",
ModLoader::Forge => "forge",
ModLoader::Vanilla => unreachable!(),
};
it.version_type == channel
&& it.game_versions.contains(&game_version.as_str())
&& it.loaders.contains(&loader_str)
})
.ok_or(ModpackError::VersionError(format!(
"Unable to find compatible version of mod {}",
project.title
)))?;
// Project fields
let envs = ModpackEnv::try_from(ManifestEnvs {
client: serde_json::from_str(project.client_side)?,
server: serde_json::from_str(project.server_side)?,
})?;
// Conversions
let files = version
.files
.iter()
.cloned()
.map(ModpackFile::from)
.collect::<HashSet<ModpackFile>>();
let dep_futures = version.dependencies.iter().map(|it| self.get_version(&it));
let deps = try_join_all(dep_futures)
.await?
.into_iter()
.flatten()
.collect::<HashSet<ModpackFile>>();
Ok(files
.into_iter()
.chain(deps.into_iter())
.map(|mut it| {
it.env = envs;
it
})
.collect())
}
async fn get_version(&self, version: &str) -> ModpackResult<HashSet<ModpackFile>> {
let version_json = try_get_json(format!("{}/api/v1/version/{}", self.0, version)).await?;
let mut version_deserializer = serde_json::Deserializer::from_slice(&version_json);
let version = ModrinthV1ProjectVersion::deserialize(&mut version_deserializer)?;
let base_path = PathBuf::from("mods/");
Ok(version
.files
.into_iter()
.map(ModpackFile::from)
.collect::<HashSet<ModpackFile>>())
}
}
// Helpers
async fn try_get_json(url: String) -> ModpackResult<Bytes> {
Ok(reqwest::get(url).await?.error_for_status()?.bytes().await?)
}

View File

@ -1,18 +1,25 @@
use std::path::{Path, PathBuf};
use daedalus::download_file_mirrors;
use futures::future;
use std::{
collections::HashSet,
hash::Hash,
path::{Path, PathBuf},
};
use tokio::fs;
use super::{
modrinth_api::{self, ModrinthV1},
ModpackResult,
};
use crate::launcher::ModLoader;
use super::ModpackResult;
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Modpack {
pub game: ModpackGame,
pub version: String,
pub name: String,
pub summary: Option<String>,
pub files: Vec<ModpackFile>,
pub files: HashSet<ModpackFile>,
}
impl Modpack {
@ -31,22 +38,84 @@ impl Modpack {
// TODO Integrate instance format to save other metadata
Ok(())
}
pub fn new(game: ModpackGame, version: &str, name: &str, summary: Option<&str>) -> Self {
Self {
game,
version: String::from(version),
name: String::from(name),
summary: summary.map(String::from),
files: HashSet::new(),
}
}
pub async fn add_project(
&mut self,
project: &str,
base_path: &Path,
source: Option<&dyn modrinth_api::ModrinthAPI>,
channel: Option<&str>,
) -> ModpackResult<()> {
let default_api = ModrinthV1(String::from("https://api.modrinth.com"));
let channel = channel.unwrap_or("release");
let source = source.unwrap_or(&default_api);
let files = source
.get_latest_version(project, channel, &self.game)
.await?
.into_iter()
.map(|mut it: ModpackFile| {
it.path = base_path.join(it.path);
it
});
self.files.extend(files);
Ok(())
}
pub async fn add_version(
&mut self,
version: &str,
base_path: &Path,
source: Option<&dyn modrinth_api::ModrinthAPI>,
) -> ModpackResult<()> {
let default_api = ModrinthV1(String::from("https://api.modrinth.com"));
let source = source.unwrap_or(&default_api);
let files = source
.get_version(version)
.await?
.into_iter()
.map(|mut it: ModpackFile| {
it.path = base_path.join(it.path);
it
});
self.files.extend(files);
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModpackGame {
// TODO: Currently, the launcher does not support specifying mod loader versions, so I just
// store the loader here.
Minecraft(String, ModLoader),
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModpackFile {
pub path: PathBuf,
pub hashes: ModpackFileHashes,
pub env: ModpackEnv,
pub downloads: Vec<String>,
pub downloads: HashSet<String>,
}
impl Hash for ModpackFile {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.hashes.sha1.hash(state);
self.path.hash(state);
}
}
impl ModpackFile {
@ -76,13 +145,19 @@ impl ModpackFile {
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModpackEnv {
ClientOnly,
ServerOnly,
Both,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModpackSide {
Client,
Server,
}
impl ModpackEnv {
pub fn supports(&self, side: ModpackSide) -> bool {
match self {
@ -93,14 +168,59 @@ impl ModpackEnv {
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ModpackSide {
Client, Server,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModpackFileHashes {
pub sha1: String,
}
#[cfg(test)]
mod tests {
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use super::*;
use crate::launcher::ModLoader;
#[tokio::test]
async fn add_version() -> ModpackResult<()> {
const TEST_VERSION: &'static str = "TpnSObJ7";
let mut test_pack = Modpack::new(
ModpackGame::Minecraft(String::from("1.16.5"), ModLoader::Fabric),
"0.1.0",
"Example Modpack",
None,
);
test_pack
.add_version(TEST_VERSION, Path::new("mods/"), None)
.await?;
assert_eq!(
test_pack,
Modpack {
game: ModpackGame::Minecraft(String::from("1.16.5"), ModLoader::Fabric),
version: String::from("0.1.0"),
name: String::from("Example Modpack"),
summary: None,
files: {
let mut files = HashSet::new();
files.insert(ModpackFile {
path: PathBuf::from("mods/gravestones-v1.9.jar"),
hashes: ModpackFileHashes {
sha1: String::from("3f0f6d523d218460310b345be03ab3f1d294e04d"),
},
env: ModpackEnv::Both,
downloads: {
let mut downloads = HashSet::new();
downloads.insert(String::from("https://cdn.modrinth.com/data/ssUbhMkL/versions/v1.9/gravestones-v1.9.jar"));
downloads
}
});
files
},
},
);
Ok(())
}
}