Added modpack creation and file adding
This commit is contained in:
parent
fe3581756f
commit
5ffddd6c8a
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -8,6 +8,7 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0"
|
||||
async-trait = "0.1.51"
|
||||
|
||||
daedalus = "0.1.6"
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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>;
|
||||
|
||||
176
theseus/src/modpack/modrinth_api.rs
Normal file
176
theseus/src/modpack/modrinth_api.rs
Normal 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?)
|
||||
}
|
||||
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user