Complete refactor of modpacks, add modpack serialization

This commit is contained in:
Daniel Hutzley 2021-12-01 21:06:31 -08:00
parent 067a51338f
commit fe3581756f
5 changed files with 446 additions and 372 deletions

73
Cargo.lock generated
View File

@ -147,9 +147,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "crc32fast"
version = "1.2.1"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
checksum = "3825b1e8580894917dc4468cb634a1b4e9745fddc854edad72d9c04644c0319f"
dependencies = [
"cfg-if",
]
@ -230,9 +230,9 @@ checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394"
[[package]]
name = "futures"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e"
dependencies = [
"futures-channel",
"futures-core",
@ -245,9 +245,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27"
dependencies = [
"futures-core",
"futures-sink",
@ -255,15 +255,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
[[package]]
name = "futures-executor"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
dependencies = [
"futures-core",
"futures-task",
@ -272,18 +272,16 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11"
[[package]]
name = "futures-macro"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb"
checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd"
dependencies = [
"autocfg",
"proc-macro-hack",
"proc-macro2",
"quote",
"syn",
@ -291,23 +289,22 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af"
[[package]]
name = "futures-task"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
[[package]]
name = "futures-util"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
dependencies = [
"autocfg",
"futures-channel",
"futures-core",
"futures-io",
@ -317,8 +314,6 @@ dependencies = [
"memchr",
"pin-project-lite",
"pin-utils",
"proc-macro-hack",
"proc-macro-nested",
"slab",
]
@ -506,9 +501,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.107"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219"
checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119"
[[package]]
name = "lock_api"
@ -734,18 +729,6 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
[[package]]
name = "proc-macro-hack"
version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro-nested"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
[[package]]
name = "proc-macro2"
version = "1.0.32"
@ -876,9 +859,9 @@ dependencies = [
[[package]]
name = "ryu"
version = "1.0.5"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568"
[[package]]
name = "schannel"
@ -941,9 +924,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.71"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19"
checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
dependencies = [
"itoa",
"ryu",
@ -1001,9 +984,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.81"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
dependencies = [
"proc-macro2",
"quote",

View File

@ -1,7 +1,7 @@
use std::{path::PathBuf, time::Instant};
use argh::FromArgs;
use theseus::modpack::{fetch_modpack, manifest::ModpackSide};
use theseus::modpack::{fetch_modpack, pack::ModpackSide};
#[derive(FromArgs)]
/// Simple modpack download

View File

@ -1,291 +1,266 @@
use std::{
convert::TryFrom,
path::{Path, PathBuf},
str::FromStr,
};
use std::path::{Path, PathBuf};
use daedalus::download_file_mirrors;
use futures::future;
use tokio::fs;
use std::convert::TryFrom;
use crate::launcher::ModLoader;
use super::ModpackError;
use super::pack::ModpackGame;
use super::{pack, ModpackError, ModpackResult};
use daedalus::modded::LoaderType;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq)]
pub struct Manifest {
format_version: u64,
game: ModpackGame,
version_id: String,
pub const DEFAULT_FORMAT_VERSION: u32 = 1;
name: String,
summary: Option<String>,
files: Vec<ModpackFile>,
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Manifest<'a> {
pub format_version: u32,
pub game: &'a str,
pub version_id: &'a str,
pub name: &'a str,
#[serde(borrow)]
pub summary: Option<&'a str>,
pub files: Vec<ManifestFile<'a>>,
pub dependencies: ManifestDeps<'a>,
}
impl Manifest {
/// Download a modpack's files for a given side to a given destination
/// Assumes the destination exists and is a directory
pub async fn download_files(&self, dest: &Path, side: ModpackSide) -> Result<(), ModpackError> {
let handles = self.files.clone().into_iter().map(move |file| {
let (dest, side) = (dest.to_owned(), side);
tokio::spawn(async move { file.fetch(&dest, side).await })
});
future::try_join_all(handles)
.await?
.into_iter()
.collect::<Result<_, ModpackError>>()?;
// TODO Integrate instance format to save other metadata
Ok(())
}
}
fn try_get<'r, F, T>(
manifest: &'r serde_json::Map<String, serde_json::Value>,
field: &str,
caster: F,
) -> Result<T, ModpackError>
where
F: Fn(&'r serde_json::Value) -> Option<T>,
{
manifest
.get(field)
.and_then(caster)
.ok_or(ModpackError::ManifestError(format!(
"Invalid or missing field: {}",
field
)))
}
impl TryFrom<serde_json::Value> for Manifest {
impl TryFrom<Manifest<'_>> for pack::Modpack {
type Error = ModpackError;
fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
use ModpackError::ManifestError;
let value = value.as_object().ok_or(ManifestError(String::from(
"Manifest is not a JSON object!",
)))?;
let game = ModpackGame::new(
try_get(value, "game", serde_json::Value::as_str)?,
try_get(value, "dependencies", serde_json::Value::as_object)?,
)?;
let files = try_get(value, "files", serde_json::Value::as_array)?
.iter()
.map(|it| -> Result<ModpackFile, ModpackError> {
let file = it
.as_object()
.ok_or(ManifestError(String::from("Malformed file: not an object")))?;
let path = Path::new(try_get(file, "path", serde_json::Value::as_str)?);
let hashes = ModpackFileHashes::try_from(try_get(
file,
"hashes",
serde_json::Value::as_object,
)?)?;
let downloads = try_get(file, "downloads", serde_json::Value::as_array)?
.iter()
.map(serde_json::Value::as_str)
.map(|it| it.map(String::from))
.collect::<Option<Vec<String>>>()
.ok_or(ManifestError(format!(
"Invalid source for path {}",
path.to_str().unwrap_or("?")
)))?;
let env: Option<[ModpackEnv; 2]> = if let Some(env) = file.get("env") {
if !env.is_object() {
return Err(ManifestError(String::from(
"Env is provided, but is not an object!",
)));
}
Some([
ModpackEnv::from_str(
env.get("client")
.and_then(serde_json::Value::as_str)
.unwrap_or_default(),
)?,
ModpackEnv::from_str(
env.get("server")
.and_then(serde_json::Value::as_str)
.unwrap_or_default(),
)?,
])
} else {
None
};
ModpackFile::new(path, hashes, env, &downloads)
})
.collect::<Result<Vec<ModpackFile>, ModpackError>>()?;
fn try_from(manifest: Manifest<'_>) -> Result<Self, Self::Error> {
let files = manifest
.files
.into_iter()
.map(pack::ModpackFile::try_from)
.collect::<ModpackResult<Vec<pack::ModpackFile>>>()?;
Ok(Self {
format_version: try_get(value, "formatVersion", serde_json::Value::as_u64)?,
game,
version_id: String::from(try_get(value, "versionId", serde_json::Value::as_str)?),
name: String::from(try_get(value, "name", serde_json::Value::as_str)?),
summary: value
.get("summary")
.and_then(serde_json::Value::as_str)
.map(String::from),
name: String::from(manifest.name),
version: String::from(manifest.version_id),
summary: manifest.summary.map(String::from),
game: ModpackGame::from(manifest.dependencies),
files,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ModpackGame {
// TODO: Currently, the launcher does not support specifying mod loader versions, so I just
// store the loader here.
Minecraft(String, ModLoader),
const MODRINTH_GAMEDATA_URL: &'static str = "https://staging-cdn.modrinth.com/gamedata";
fn get_loader_version(loader: ModLoader, version: &str) -> ModpackResult<String> {
let source = match loader {
ModLoader::Vanilla => Err(ModpackError::VersionError(String::from(
"Attempted to get mod loader version of Vanilla",
))),
ModLoader::Forge => Ok(format!("{}/forge/v0/manifest.json", MODRINTH_GAMEDATA_URL)),
ModLoader::Fabric => Ok(format!("{}/fabric/v0/manifest.json", MODRINTH_GAMEDATA_URL)),
}?;
let manifest = futures::executor::block_on(daedalus::modded::fetch_manifest(&source))?;
Ok(manifest
.game_versions
.iter()
.find(|&it| it.id == version)
.ok_or(ModpackError::VersionError(format!(
"No versions of {:?} exist for Minecraft {}",
loader, version
)))?
.loaders[&LoaderType::Latest]
.id
.clone())
}
impl ModpackGame {
pub fn new(
game: &str,
deps: &serde_json::Map<String, serde_json::Value>,
) -> Result<Self, ModpackError> {
match game {
"minecraft" => {
let game_version = String::from(
deps.get("minecraft")
.ok_or(ModpackError::ManifestError(String::from(
"No version of minecraft given",
)))?
.as_str()
.unwrap(),
);
// TODO: See comment in ModpackGame, this code was designed specifically to be
// easily adapted for versioned modloaders
let loader = if let Some(_) = deps.get("fabric-loader") {
ModLoader::Fabric
} else if let Some(_) = deps.get("forge") {
ModLoader::Forge
} else {
ModLoader::Vanilla
};
Ok(ModpackGame::Minecraft(game_version, loader))
}
_ => Err(ModpackError::ManifestError(format!(
"Invalid game: {}",
game
))),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ModpackFile {
path: PathBuf,
hashes: ModpackFileHashes,
envs: Option<[ModpackEnv; 2]>,
downloads: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ModpackSide {
Client = 0,
Server = 1,
}
impl ModpackFile {
pub fn new(
path: &Path,
hashes: ModpackFileHashes,
envs: Option<[ModpackEnv; 2]>,
downloads: &[String],
) -> Result<Self, ModpackError> {
if path.is_dir() {
return Err(ModpackError::ManifestError(format!(
"Modpack file {} is a directory!",
path.to_str().unwrap_or("?")
)));
}
Ok(Self {
path: PathBuf::from(path),
hashes,
envs,
downloads: Vec::from(downloads),
})
}
pub async fn fetch(&self, dest: &Path, side: ModpackSide) -> Result<(), ModpackError> {
if let Some(envs) = &self.envs {
if envs[side as usize] == ModpackEnv::Unsupported
|| envs[(side as usize + 1) % 2] == ModpackEnv::Required
{
return Ok(());
}
}
let output = dest.join(&self.path);
// HACK: Since Daedalus appends a file name to all mirrors and the manifest supplies full
// URLs, I'm supplying it with an empty string to avoid reinventing the wheel.
let bytes = download_file_mirrors(
"",
&self
.downloads
.iter()
.map(|it| it.as_str())
.collect::<Vec<&str>>()
.as_slice(),
Some(&self.hashes.sha1),
)
.await?;
fs::create_dir_all(output.parent().unwrap()).await?;
fs::write(output, bytes).await?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ModpackFileHashes {
sha1: String,
}
impl TryFrom<&serde_json::Map<String, serde_json::Value>> for ModpackFileHashes {
impl<'a> TryFrom<&'a pack::Modpack> for Manifest<'a> {
type Error = ModpackError;
fn try_from(value: &serde_json::Map<String, serde_json::Value>) -> Result<Self, Self::Error> {
let sha1 = String::from(try_get(&value, "sha1", serde_json::Value::as_str)?);
Ok(Self { sha1 })
fn try_from(pack: &'a pack::Modpack) -> Result<Self, Self::Error> {
let game_field: &'a str = match pack.game {
ModpackGame::Minecraft(..) => "minecraft",
};
let files = pack
.files
.iter()
.map(ManifestFile::from)
.collect::<Vec<ManifestFile>>();
Ok(Manifest {
format_version: DEFAULT_FORMAT_VERSION,
game: game_field,
version_id: &pack.version,
name: &pack.name,
summary: pack.summary.as_ref().map(String::as_str),
files,
dependencies: ManifestDeps::try_from(&pack.game)?,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ModpackEnv {
#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ManifestFile<'a> {
#[serde(borrow)]
pub path: &'a Path,
pub hashes: ManifestHashes<'a>,
#[serde(default)]
pub env: ManifestEnvs,
#[serde(borrow)]
pub downloads: Vec<&'a str>,
}
impl TryFrom<ManifestFile<'_>> for pack::ModpackFile {
type Error = ModpackError;
fn try_from(file: ManifestFile<'_>) -> Result<Self, Self::Error> {
Ok(Self {
path: PathBuf::from(file.path),
hashes: pack::ModpackFileHashes::from(file.hashes),
env: pack::ModpackEnv::try_from(file.env)?,
downloads: file.downloads.into_iter().map(ToOwned::to_owned).collect(),
})
}
}
impl<'a> From<&'a pack::ModpackFile> for ManifestFile<'a> {
fn from(file: &'a pack::ModpackFile) -> Self {
Self {
path: file.path.as_path(),
hashes: (&file.hashes).into(),
env: file.env.into(),
downloads: file
.downloads
.iter()
.map(String::as_str)
.collect::<Vec<&str>>(),
}
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct ManifestHashes<'a> {
pub sha1: &'a str,
}
impl From<ManifestHashes<'_>> for pack::ModpackFileHashes {
fn from(hashes: ManifestHashes<'_>) -> Self {
Self {
sha1: String::from(hashes.sha1),
}
}
}
impl<'a> From<&'a pack::ModpackFileHashes> for ManifestHashes<'a> {
fn from(hashes: &'a pack::ModpackFileHashes) -> Self {
Self { sha1: &hashes.sha1 }
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct ManifestEnvs {
pub client: ManifestEnv,
pub server: ManifestEnv,
}
impl Default for ManifestEnvs {
fn default() -> Self {
Self {
client: ManifestEnv::Optional,
server: ManifestEnv::Optional,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ManifestEnv {
Required,
Optional,
Unsupported,
}
impl FromStr for ModpackEnv {
type Err = ModpackError;
impl TryFrom<ManifestEnvs> for pack::ModpackEnv {
type Error = ModpackError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use ModpackEnv::*;
match s {
"required" => Ok(Required),
"optional" => Ok(Optional),
"unsupported" => Ok(Unsupported),
_ => Err(ModpackError::ManifestError(format!(
"Invalid environment support: {}",
s
fn try_from(envs: ManifestEnvs) -> Result<Self, Self::Error> {
use ManifestEnv::*;
match (envs.client, envs.server) {
(Required, Unsupported) => Ok(Self::ClientOnly),
(Unsupported, Required) => Ok(Self::ServerOnly),
(Optional, Optional) => Ok(Self::Both),
_ => Err(ModpackError::FormatError(format!(
"Invalid environment specification: {:?}",
envs
))),
}
}
}
impl Default for ModpackEnv {
fn default() -> Self {
Self::Optional
impl From<pack::ModpackEnv> for ManifestEnvs {
fn from(envs: pack::ModpackEnv) -> Self {
use super::pack::ModpackEnv::*;
let (client, server) = match envs {
ClientOnly => (ManifestEnv::Required, ManifestEnv::Unsupported),
ServerOnly => (ManifestEnv::Unsupported, ManifestEnv::Required),
Both => (ManifestEnv::Optional, ManifestEnv::Optional),
};
Self { client, server }
}
}
#[derive(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.
pub enum ManifestDeps<'a> {
MinecraftFabric {
minecraft: &'a str,
#[serde(rename = "fabric-loader")]
fabric_loader: String,
},
MinecraftForge {
minecraft: &'a str,
forge: String,
},
MinecraftVanilla {
minecraft: &'a str,
},
}
impl From<ManifestDeps<'_>> for pack::ModpackGame {
fn from(deps: ManifestDeps<'_>) -> Self {
use ManifestDeps::*;
match deps {
MinecraftVanilla { minecraft } => {
Self::Minecraft(String::from(minecraft), ModLoader::Vanilla)
}
MinecraftFabric { minecraft, .. } => {
Self::Minecraft(String::from(minecraft), ModLoader::Fabric)
}
MinecraftForge { minecraft, .. } => {
Self::Minecraft(String::from(minecraft), ModLoader::Forge)
}
}
}
}
impl<'a> TryFrom<&'a pack::ModpackGame> for ManifestDeps<'a> {
type Error = ModpackError;
fn try_from(game: &'a pack::ModpackGame) -> Result<Self, Self::Error> {
use super::pack::ModpackGame::*;
Ok(match game {
Minecraft(ref ver, ModLoader::Vanilla) => Self::MinecraftVanilla { minecraft: ver },
Minecraft(ref ver, loader @ ModLoader::Fabric) => Self::MinecraftFabric {
minecraft: ver,
fabric_loader: get_loader_version(*loader, ver)?,
},
Minecraft(ref ver, loader @ ModLoader::Forge) => Self::MinecraftForge {
minecraft: ver,
forge: get_loader_version(*loader, ver)?,
},
})
}
}
@ -294,7 +269,7 @@ mod tests {
use super::*;
#[test]
fn parse_simple() -> Result<(), ModpackError> {
fn parse_simple() -> ModpackResult<()> {
const PACK_JSON: &'static str = r#"
{
"formatVersion": 1,
@ -309,22 +284,23 @@ mod tests {
"#;
let expected_manifest = Manifest {
format_version: 1,
game: ModpackGame::Minecraft(String::from("1.17.1"), ModLoader::Vanilla),
version_id: String::from("deadbeef"),
name: String::from("Example Pack"),
game: "minecraft",
version_id: "deadbeef",
name: "Example Pack",
summary: None,
files: Vec::new(),
dependencies: ManifestDeps::MinecraftVanilla {
minecraft: "1.17.1",
},
};
let manifest_json: serde_json::Value =
serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
let manifest = Manifest::try_from(manifest_json)?;
let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
assert_eq!(expected_manifest, manifest);
Ok(())
}
#[test]
fn parse_forge() -> Result<(), ModpackError> {
fn parse_forge() -> ModpackResult<()> {
const PACK_JSON: &'static str = r#"
{
"formatVersion": 1,
@ -348,33 +324,33 @@ mod tests {
}
}
"#;
let expected_manifest = Manifest {
format_version: 1,
game: ModpackGame::Minecraft(String::from("1.17.1"), ModLoader::Forge),
version_id: String::from("deadbeef"),
name: String::from("Example Pack"),
game: "minecraft",
version_id: "deadbeef",
name: "Example Pack",
summary: None,
files: vec![ModpackFile::new(
Path::new("mods/testmod.jar"),
ModpackFileHashes {
sha1: String::from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
files: vec![ManifestFile {
path: Path::new("mods/testmod.jar"),
hashes: ManifestHashes {
sha1: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
None,
&[String::from("https://example.com/testmod.jar")],
)?],
env: ManifestEnvs::default(),
downloads: vec!["https://example.com/testmod.jar"],
}],
dependencies: ManifestDeps::MinecraftForge {
minecraft: "1.17.1",
forge: String::from("37.0.110"),
},
};
let manifest_json: serde_json::Value =
serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
let manifest = Manifest::try_from(manifest_json)?;
let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
assert_eq!(expected_manifest, manifest);
Ok(())
}
#[test]
fn parse_fabric() -> Result<(), ModpackError> {
fn parse_fabric() -> ModpackResult<()> {
const PACK_JSON: &'static str = r#"
{
"formatVersion": 1,
@ -398,33 +374,33 @@ mod tests {
}
}
"#;
let expected_manifest = Manifest {
format_version: 1,
game: ModpackGame::Minecraft(String::from("1.17.1"), ModLoader::Fabric),
version_id: String::from("deadbeef"),
name: String::from("Example Pack"),
game: "minecraft",
version_id: "deadbeef",
name: "Example Pack",
summary: None,
files: vec![ModpackFile::new(
Path::new("mods/testmod.jar"),
ModpackFileHashes {
sha1: String::from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
files: vec![ManifestFile {
path: Path::new("mods/testmod.jar"),
hashes: ManifestHashes {
sha1: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
None,
&[String::from("https://example.com/testmod.jar")],
)?],
env: ManifestEnvs::default(),
downloads: vec!["https://example.com/testmod.jar"],
}],
dependencies: ManifestDeps::MinecraftFabric {
minecraft: "1.17.1",
fabric_loader: String::from("0.9.0"),
},
};
let manifest_json: serde_json::Value =
serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
let manifest = Manifest::try_from(manifest_json)?;
let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
assert_eq!(expected_manifest, manifest);
Ok(())
}
#[test]
fn parse_complete() -> Result<(), ModpackError> {
fn parse_complete() -> ModpackResult<()> {
const PACK_JSON: &'static str = r#"
{
"formatVersion": 1,
@ -453,28 +429,29 @@ mod tests {
}
}
"#;
let expected_manifest = Manifest {
format_version: 1,
game: ModpackGame::Minecraft(String::from("1.17.1"), ModLoader::Forge),
version_id: String::from("deadbeef"),
name: String::from("Example Pack"),
summary: Some(String::from("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")),
files: vec![
ModpackFile::new(
Path::new("mods/testmod.jar"),
ModpackFileHashes {
sha1: String::from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
},
Some([ ModpackEnv::Required, ModpackEnv::Unsupported ]),
&[ String::from("https://example.com/testmod.jar") ],
)?
],
game: "minecraft",
version_id: "deadbeef",
name: "Example Pack",
summary: Some("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."),
files: vec![ManifestFile {
path: Path::new("mods/testmod.jar"),
hashes: ManifestHashes {
sha1: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
env: ManifestEnvs {
client: ManifestEnv::Required,
server: ManifestEnv::Unsupported,
},
downloads: vec!["https://example.com/testmod.jar"],
}],
dependencies: ManifestDeps::MinecraftForge {
minecraft: "1.17.1",
forge: String::from("37.0.110"),
},
};
let manifest_json: serde_json::Value =
serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
let manifest = Manifest::try_from(manifest_json)?;
let manifest: Manifest = serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
assert_eq!(expected_manifest, manifest);
Ok(())

View File

@ -2,14 +2,16 @@
use daedalus::download_file;
use fs_extra::dir::CopyOptions;
use std::{borrow::Borrow, convert::TryFrom, env, io, path::Path};
use serde::Deserialize;
use std::{convert::TryFrom, env, io, path::Path};
use tokio::fs;
use uuid::Uuid;
use zip::ZipArchive;
use self::manifest::Manifest;
use self::{manifest::Manifest, pack::Modpack};
pub mod manifest;
pub mod pack;
mod manifest;
pub const MANIFEST_PATH: &'static str = "index.json";
pub const OVERRIDES_PATH: &'static str = "overrides/";
@ -42,15 +44,20 @@ pub enum ModpackError {
#[error("Error joining futures: {0}")]
JoinError(#[from] tokio::task::JoinError),
#[error("Error fetching modloader version: {0}")]
VersionError(String),
}
type ModpackResult<T> = Result<T, ModpackError>;
/// Realise a modpack from a given URL
pub async fn fetch_modpack(
url: &str,
sha1: Option<&str>,
dest: &Path,
side: manifest::ModpackSide,
) -> Result<(), ModpackError> {
side: pack::ModpackSide,
) -> ModpackResult<()> {
let bytes = download_file(url, sha1).await?;
let mut archive = ZipArchive::new(io::Cursor::new(&bytes as &[u8]))?;
realise_modpack_zip(&mut archive, dest, side).await
@ -60,8 +67,8 @@ pub async fn fetch_modpack(
pub async fn realise_modpack_zip(
archive: &mut ZipArchive<impl io::Read + io::Seek>,
dest: &Path,
side: manifest::ModpackSide,
) -> Result<(), ModpackError> {
side: pack::ModpackSide,
) -> ModpackResult<()> {
let tmp = env::temp_dir().join(format!("theseus-{}/", Uuid::new_v4()));
archive.extract(&tmp)?;
realise_modpack(&tmp, dest, side).await
@ -71,8 +78,8 @@ pub async fn realise_modpack_zip(
pub async fn realise_modpack(
dir: &Path,
dest: &Path,
side: manifest::ModpackSide,
) -> Result<(), ModpackError> {
side: pack::ModpackSide,
) -> ModpackResult<()> {
if dest.is_file() {
return Err(ModpackError::InvalidDirectory(String::from(
"Output is not a directory",
@ -101,11 +108,12 @@ pub async fn realise_modpack(
"Manifest missing or is not a file",
)))?;
let manifest_file = std::fs::File::open(manifest_path)?;
let manifest_json: serde_json::Value =
serde_json::from_reader(io::BufReader::new(manifest_file))?;
let manifest = Manifest::try_from(manifest_json)?;
let reader = io::BufReader::new(manifest_file);
let mut deserializer = serde_json::Deserializer::from_reader(reader);
let manifest = Manifest::deserialize(&mut deserializer)?;
let modpack = Modpack::try_from(manifest)?;
// Realise manifest
manifest.download_files(dest, side).await?;
// Realise modpack
modpack.download_files(dest, side).await?;
Ok(())
}

106
theseus/src/modpack/pack.rs Normal file
View File

@ -0,0 +1,106 @@
use std::path::{Path, PathBuf};
use daedalus::download_file_mirrors;
use futures::future;
use tokio::fs;
use crate::launcher::ModLoader;
use super::ModpackResult;
#[derive(Debug, Clone, PartialEq)]
pub struct Modpack {
pub game: ModpackGame,
pub version: String,
pub name: String,
pub summary: Option<String>,
pub files: Vec<ModpackFile>,
}
impl Modpack {
/// Download a modpack's files for a given side to a given destination
/// Assumes the destination exists and is a directory
pub async fn download_files(&self, dest: &Path, side: ModpackSide) -> ModpackResult<()> {
let handles = self.files.iter().cloned().map(move |file| {
let (dest, side) = (dest.to_owned(), side);
tokio::spawn(async move { file.fetch(&dest, side).await })
});
future::try_join_all(handles)
.await?
.into_iter()
.collect::<ModpackResult<_>>()?;
// TODO Integrate instance format to save other metadata
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
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)]
pub struct ModpackFile {
pub path: PathBuf,
pub hashes: ModpackFileHashes,
pub env: ModpackEnv,
pub downloads: Vec<String>,
}
impl ModpackFile {
pub async fn fetch(&self, dest: &Path, side: ModpackSide) -> ModpackResult<()> {
if !self.env.supports(side) {
return Ok(());
}
let output = dest.join(&self.path);
// HACK: Since Daedalus appends a file name to all mirrors and the manifest supplies full
// URLs, I'm supplying it with an empty string to avoid reinventing the wheel.
let bytes = download_file_mirrors(
"",
&self
.downloads
.iter()
.map(|it| it.as_str())
.collect::<Vec<&str>>()
.as_slice(),
Some(&self.hashes.sha1),
)
.await?;
fs::create_dir_all(output.parent().unwrap()).await?;
fs::write(output, bytes).await?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ModpackEnv {
ClientOnly,
ServerOnly,
Both,
}
impl ModpackEnv {
pub fn supports(&self, side: ModpackSide) -> bool {
match self {
Self::ClientOnly => side == ModpackSide::Client,
Self::ServerOnly => side == ModpackSide::Server,
Self::Both => true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ModpackSide {
Client, Server,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ModpackFileHashes {
pub sha1: String,
}