init
This commit is contained in:
commit
dd97213de9
18 changed files with 914 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
target/
|
||||
Cargo.lock
|
5
Cargo.toml
Normal file
5
Cargo.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"mpt",
|
||||
"addonscript",
|
||||
]
|
11
addonscript/Cargo.toml
Normal file
11
addonscript/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "addonscript"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.129", features = ["derive"] }
|
||||
thiserror = "1.0.26"
|
||||
url = { version = "2.2.2", features = ["serde"] }
|
2
addonscript/src/lib.rs
Normal file
2
addonscript/src/lib.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod manifest;
|
||||
pub mod util;
|
96
addonscript/src/manifest/installer.rs
Normal file
96
addonscript/src/manifest/installer.rs
Normal file
|
@ -0,0 +1,96 @@
|
|||
use serde::{de::Visitor, Deserializer};
|
||||
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
use thiserror::Error;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Installer {
|
||||
Override,
|
||||
Dir(PathBuf),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum InstallerParseError {
|
||||
#[error("Unknown Installer type")]
|
||||
UnknownType,
|
||||
#[error("Invalid installer arguments")]
|
||||
InvalidArguments,
|
||||
#[error("Invalid installer syntax")]
|
||||
InvalidSyntax,
|
||||
}
|
||||
|
||||
impl FromStr for Installer {
|
||||
type Err = InstallerParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut splits = s.split(':');
|
||||
let installer_type = splits.next().ok_or(InstallerParseError::InvalidSyntax)?;
|
||||
|
||||
match installer_type {
|
||||
"internal.override" => {
|
||||
if splits.count() == 0 {
|
||||
Ok(Installer::Override)
|
||||
} else {
|
||||
Err(InstallerParseError::InvalidArguments)
|
||||
}
|
||||
},
|
||||
"internal.dir" => {
|
||||
let dir = splits.next().ok_or(InstallerParseError::InvalidArguments)?;
|
||||
let dir = PathBuf::from(dir);
|
||||
|
||||
if splits.count() == 0 {
|
||||
Ok(Installer::Dir(dir))
|
||||
} else {
|
||||
Err(InstallerParseError::InvalidArguments)
|
||||
}
|
||||
},
|
||||
_ => Err(InstallerParseError::UnknownType),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Installer {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Installer::Override => String::from("internal.override"),
|
||||
Installer::Dir(dir) => format!("internal.dir:{}", dir.to_string_lossy()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Installer {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Installer {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct Vis;
|
||||
impl<'de> Visitor<'de> for Vis {
|
||||
type Value = Installer;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("an AddonScript installer")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
v.parse::<Installer>()
|
||||
.map_err(|e| E::custom(format!("invalid installer: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(Vis)
|
||||
}
|
||||
}
|
81
addonscript/src/manifest/link.rs
Normal file
81
addonscript/src/manifest/link.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use serde::{de::Visitor, Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Link {
|
||||
File(PathBuf),
|
||||
Http(Url),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LinkParseError {
|
||||
#[error("Unknown protocol")]
|
||||
UnkownProtocol,
|
||||
#[error("Error parsing url: {0}")]
|
||||
UrlParseError(url::ParseError),
|
||||
}
|
||||
|
||||
impl FromStr for Link {
|
||||
type Err = LinkParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.starts_with("file://") {
|
||||
Ok(Link::File(PathBuf::from(s.trim_start_matches("file://"))))
|
||||
} else if s.starts_with("http://") || s.starts_with("https://") {
|
||||
s.parse::<Url>()
|
||||
.map(Link::Http)
|
||||
.map_err(LinkParseError::UrlParseError)
|
||||
} else {
|
||||
Err(LinkParseError::UnkownProtocol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Link {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Link::File(path) => format!("file://{}", path.to_string_lossy()),
|
||||
Link::Http(url) => url.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Link {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Link {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct Vis;
|
||||
impl<'de> Visitor<'de> for Vis {
|
||||
type Value = Link;
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str(
|
||||
"either a (somewhat invalid) relative file:// url or a http/https url",
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
v.parse::<Link>()
|
||||
.map_err(|e| E::custom(format!("invalid link: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(Vis)
|
||||
}
|
||||
}
|
100
addonscript/src/manifest/mod.rs
Normal file
100
addonscript/src/manifest/mod.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
use crate::manifest::{installer::Installer, link::Link};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use url::Url;
|
||||
|
||||
pub mod installer;
|
||||
pub mod link;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct Manifest {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub manifest_type: ManifestType,
|
||||
pub versions: Vec<Version>,
|
||||
pub repositories: Vec<Repository>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct Repository {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub repo_type: RepositoryType,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RepositoryType {
|
||||
Maven,
|
||||
Curseforge,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct Version {
|
||||
pub version: String,
|
||||
pub mcversion: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub files: Vec<File>,
|
||||
#[serde(default)]
|
||||
pub relations: Vec<Relation>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct Relation {
|
||||
pub id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub file: Option<File>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub versions: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
pub relation_type: RelationType,
|
||||
pub options: HashSet<FileOpt>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RelationType {
|
||||
Mod,
|
||||
Modloader,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Hash, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FileOpt {
|
||||
Required,
|
||||
Client,
|
||||
Server,
|
||||
Included,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum File {
|
||||
Link {
|
||||
id: Option<String>,
|
||||
installer: Installer,
|
||||
link: Link,
|
||||
options: Option<HashSet<FileOpt>>,
|
||||
},
|
||||
Maven {
|
||||
installer: Installer,
|
||||
artifact: String,
|
||||
repository: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl File {
|
||||
pub fn installer(&self) -> &Installer {
|
||||
match self {
|
||||
File::Link { installer, .. } => installer,
|
||||
File::Maven { installer, .. } => installer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ManifestType {
|
||||
Modpack,
|
||||
}
|
14
addonscript/src/util.rs
Normal file
14
addonscript/src/util.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use crate::manifest::FileOpt;
|
||||
|
||||
pub fn default_file_opts() -> HashSet<FileOpt> {
|
||||
let mut set = HashSet::with_capacity(4);
|
||||
|
||||
set.insert(FileOpt::Client);
|
||||
set.insert(FileOpt::Server);
|
||||
set.insert(FileOpt::Required);
|
||||
set.insert(FileOpt::Included);
|
||||
|
||||
set
|
||||
}
|
26
mpt/Cargo.toml
Normal file
26
mpt/Cargo.toml
Normal file
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "mpt"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[[bin]]
|
||||
name = "lmpt"
|
||||
path = "src/main.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
addonscript = { path = "../addonscript"}
|
||||
anyhow = "1.0.43"
|
||||
crossterm = "0.21.0"
|
||||
futures = "0.3.16"
|
||||
indicatif = "0.16.2"
|
||||
json5 = "0.3.0"
|
||||
reqwest = { version = "0.11.4", features = ["stream"] }
|
||||
serde = { version = "1.0.129", features = ["derive"] }
|
||||
serde_json = "1.0.67"
|
||||
structopt = "0.3.22"
|
||||
thiserror = "1.0.28"
|
||||
tokio = { version = "1.10.1", features = ["rt-multi-thread", "macros", "fs"] }
|
||||
toml = "0.5.8"
|
||||
url = "2.2.2"
|
11
mpt/assets/modpacktoolsconfig.toml
Normal file
11
mpt/assets/modpacktoolsconfig.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[Locations]
|
||||
# The location of the source
|
||||
src = "src"
|
||||
# The location used to store temporary files
|
||||
tempDir = ".mpt"
|
||||
|
||||
[Downloads]
|
||||
# The maximum number of threads that will be used for downloads
|
||||
maxThreads = 5
|
||||
# The timeout of http requests in ms
|
||||
httpTimeout = 30000
|
201
mpt/src/commands/downloadmods.rs
Normal file
201
mpt/src/commands/downloadmods.rs
Normal file
|
@ -0,0 +1,201 @@
|
|||
use crate::{config::Config, util::mvn_artifact_to_url};
|
||||
use addonscript::manifest::{
|
||||
installer::Installer,
|
||||
link::Link,
|
||||
File,
|
||||
FileOpt,
|
||||
Manifest,
|
||||
Relation,
|
||||
Repository,
|
||||
RepositoryType,
|
||||
};
|
||||
use anyhow::{bail, Context};
|
||||
use crossterm::style::{Attribute, Color, Stylize};
|
||||
use futures::stream::{self, StreamExt};
|
||||
use indicatif::ProgressBar;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{fs::File as TokioFile, io::AsyncWriteExt};
|
||||
use url::Url;
|
||||
|
||||
pub async fn run(
|
||||
(config, manifest): (Config, Manifest),
|
||||
dir: PathBuf,
|
||||
all: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let http = Arc::new(Client::new());
|
||||
let Manifest {
|
||||
mut versions,
|
||||
repositories,
|
||||
..
|
||||
} = manifest;
|
||||
let repositories = Arc::new(repositories);
|
||||
|
||||
let futs = versions
|
||||
.pop()
|
||||
.context("Manifest has no versions!")?
|
||||
.relations
|
||||
.into_iter()
|
||||
.filter(|rel| {
|
||||
rel.options.contains(&FileOpt::Client)
|
||||
&& rel
|
||||
.file
|
||||
.as_ref()
|
||||
.map(|f| {
|
||||
matches!(f.installer(), Installer::Dir(dir) if all || dir == Path::new("mods"))
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|rel| download_file(Arc::clone(&http), Arc::clone(&repositories), dir.clone(), all, rel))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let pb = ProgressBar::new(futs.len() as u64);
|
||||
let mut futs = stream::iter(futs).buffer_unordered(config.downloads.max_threads as usize);
|
||||
|
||||
while let Some(res) = futs.next().await {
|
||||
match res {
|
||||
Ok(i) => match i {
|
||||
DownloadInfo::Local { from, to } => pb.println(format!(
|
||||
"{} {} {} {}",
|
||||
"Copied local file".with(Color::Green),
|
||||
from.with(Color::Cyan).attribute(Attribute::Bold),
|
||||
"to".with(Color::Green),
|
||||
to.with(Color::Cyan).attribute(Attribute::Bold),
|
||||
)),
|
||||
DownloadInfo::Http { status, from, to } => pb.println(format!(
|
||||
"{} {} {} {} {} {}",
|
||||
"Downloaded file".with(Color::Green),
|
||||
from.with(Color::Cyan).attribute(Attribute::Bold),
|
||||
"to".with(Color::Green),
|
||||
to.with(Color::Cyan).attribute(Attribute::Bold),
|
||||
"with status".with(Color::Green),
|
||||
status
|
||||
.to_string()
|
||||
.with(Color::Cyan)
|
||||
.attribute(Attribute::Bold),
|
||||
)),
|
||||
},
|
||||
Err(e) => {
|
||||
pb.println(format!(
|
||||
"{} {:?}",
|
||||
"Error downloading file:"
|
||||
.with(Color::Red)
|
||||
.attribute(Attribute::Bold),
|
||||
e,
|
||||
));
|
||||
},
|
||||
}
|
||||
pb.inc(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum DownloadInfo {
|
||||
Local {
|
||||
from: String,
|
||||
to: String,
|
||||
},
|
||||
Http {
|
||||
status: StatusCode,
|
||||
from: String,
|
||||
to: String,
|
||||
},
|
||||
}
|
||||
|
||||
async fn download_file(
|
||||
http: Arc<Client>,
|
||||
repos: Arc<Vec<Repository>>,
|
||||
mut target_dir: PathBuf,
|
||||
all: bool,
|
||||
rel: Relation,
|
||||
) -> anyhow::Result<DownloadInfo> {
|
||||
let link;
|
||||
let link = match &rel.file {
|
||||
Some(File::Link { link, .. }) => link,
|
||||
Some(File::Maven {
|
||||
artifact,
|
||||
repository,
|
||||
..
|
||||
}) => {
|
||||
if let Some(repo) = repos.iter().find(|r| &r.id == repository) {
|
||||
match repo.repo_type {
|
||||
RepositoryType::Maven => {
|
||||
link = Link::Http(mvn_artifact_to_url(&artifact, &repo)?);
|
||||
&link
|
||||
},
|
||||
RepositoryType::Curseforge => {
|
||||
let mut splits = artifact.split(':').skip(1);
|
||||
|
||||
let p_id = splits
|
||||
.next()
|
||||
.context("Couldn't parse curseforge artifact!")?;
|
||||
let f_id = splits
|
||||
.next()
|
||||
.context("Couldn't parse curseforge artifact!")?;
|
||||
|
||||
let url = format!(
|
||||
"https://addons-ecs.forgesvc.net/api/v2/addon/{}/file/{}/download-url",
|
||||
p_id, f_id
|
||||
);
|
||||
|
||||
link = Link::Http(
|
||||
Url::parse(http.get(url).send().await?.text().await?.trim())
|
||||
.context("failed to parse curseforge URL")?,
|
||||
);
|
||||
&link
|
||||
},
|
||||
}
|
||||
} else {
|
||||
bail!("Rel {:?} references non-existant repository!")
|
||||
}
|
||||
},
|
||||
None => bail!("Rel {:?} has no file!", rel),
|
||||
};
|
||||
|
||||
if let (true, Installer::Dir(d)) = (all, rel.file.as_ref().unwrap().installer()) {
|
||||
target_dir.push(d);
|
||||
}
|
||||
|
||||
tokio::fs::create_dir_all(&target_dir).await?;
|
||||
|
||||
match link {
|
||||
Link::File(f) => {
|
||||
let to = target_dir.join(f.file_name().context("File to copy is not a file!")?);
|
||||
tokio::fs::copy(
|
||||
f,
|
||||
target_dir.join(f.file_name().context("File to copy is not a file!")?),
|
||||
)
|
||||
.await
|
||||
.context("Failed to install file with file link.")?;
|
||||
|
||||
Ok(DownloadInfo::Local {
|
||||
from: f.to_string_lossy().to_string(),
|
||||
to: to.to_string_lossy().to_string(),
|
||||
})
|
||||
},
|
||||
Link::Http(l) => {
|
||||
let res = http.get(l.clone()).send().await?;
|
||||
let p = target_dir.join(
|
||||
Path::new(l.path())
|
||||
.file_name()
|
||||
.context("HTTP File has no file name!")?,
|
||||
);
|
||||
let mut file = TokioFile::create(&p).await?;
|
||||
let status = res.status();
|
||||
let mut stream = res.bytes_stream();
|
||||
while let Some(b) = stream.next().await {
|
||||
file.write_all_buf(&mut b?).await?;
|
||||
}
|
||||
|
||||
Ok(DownloadInfo::Http {
|
||||
status,
|
||||
from: l.to_string(),
|
||||
to: p.to_string_lossy().to_string(),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
168
mpt/src/commands/init.rs
Normal file
168
mpt/src/commands/init.rs
Normal file
|
@ -0,0 +1,168 @@
|
|||
use crate::{config::Locations, forge};
|
||||
use addonscript::{
|
||||
manifest::{
|
||||
installer::Installer,
|
||||
link::Link,
|
||||
File,
|
||||
Manifest,
|
||||
ManifestType,
|
||||
Relation,
|
||||
RelationType,
|
||||
Repository,
|
||||
RepositoryType,
|
||||
Version,
|
||||
},
|
||||
util::default_file_opts,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use crossterm::{
|
||||
execute,
|
||||
style::{
|
||||
Attribute,
|
||||
Color,
|
||||
Print,
|
||||
PrintStyledContent,
|
||||
ResetColor,
|
||||
SetAttribute,
|
||||
SetForegroundColor,
|
||||
Stylize,
|
||||
},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use reqwest::Client;
|
||||
use std::path::Path;
|
||||
use url::Url;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
const DEFAULT_CONFIG: &[u8] = include_bytes!("../../assets/modpacktoolsconfig.toml");
|
||||
|
||||
pub async fn run(modpack_name: String, mcversion: String) -> anyhow::Result<()> {
|
||||
let mut stdout = std::io::stdout();
|
||||
|
||||
execute!(
|
||||
stdout,
|
||||
SetForegroundColor(Color::Green),
|
||||
Print("Creating modpack "),
|
||||
SetForegroundColor(Color::Cyan),
|
||||
SetAttribute(Attribute::Bold),
|
||||
Print(&modpack_name),
|
||||
ResetColor,
|
||||
SetForegroundColor(Color::Green),
|
||||
Print(" on minecraft version "),
|
||||
SetForegroundColor(Color::Cyan),
|
||||
SetAttribute(Attribute::Bold),
|
||||
Print(&mcversion),
|
||||
ResetColor,
|
||||
Print('\n'),
|
||||
)?;
|
||||
|
||||
let config_path = Path::new("modpacktoolsconfig.toml");
|
||||
if !config_path.exists() {
|
||||
tokio::fs::write(config_path, DEFAULT_CONFIG).await?;
|
||||
|
||||
stdout.execute(PrintStyledContent(
|
||||
"Created config!\n"
|
||||
.with(Color::Green)
|
||||
.attribute(Attribute::Bold),
|
||||
))?;
|
||||
} else {
|
||||
stdout.execute(PrintStyledContent(
|
||||
"Config already exists, skipping...\n"
|
||||
.with(Color::Red)
|
||||
.attribute(Attribute::Italic),
|
||||
))?;
|
||||
}
|
||||
|
||||
let config = tokio::fs::read(config_path).await?;
|
||||
let Config {
|
||||
locations: Locations { src, .. },
|
||||
..
|
||||
} = toml::from_slice::<Config>(&config).context("failed to deserialize config")?;
|
||||
|
||||
let path = Path::new(&src);
|
||||
|
||||
if path.join("modpack.json").exists() || path.join("modpack.json5").exists() {
|
||||
stdout.execute(PrintStyledContent(
|
||||
"Manifest already exists, skipping...\n"
|
||||
.with(Color::Red)
|
||||
.attribute(Attribute::Italic),
|
||||
))?;
|
||||
} else {
|
||||
let mut relations = vec![];
|
||||
stdout.execute(PrintStyledContent(
|
||||
"Trying to find newest forge version...\n".with(Color::Magenta),
|
||||
))?;
|
||||
|
||||
if let Some(ver) = forge::newest_forge_version(&Client::new(), &mcversion).await? {
|
||||
execute!(
|
||||
stdout,
|
||||
SetForegroundColor(Color::Green),
|
||||
Print("Found newest forge version "),
|
||||
SetForegroundColor(Color::Cyan),
|
||||
SetAttribute(Attribute::Bold),
|
||||
Print(&ver),
|
||||
ResetColor,
|
||||
Print('\n'),
|
||||
)?;
|
||||
|
||||
relations.push(Relation {
|
||||
id: "forge".into(),
|
||||
file: None,
|
||||
relation_type: RelationType::Modloader,
|
||||
options: default_file_opts(),
|
||||
versions: Some(format!(
|
||||
"[{mcver}-{forgever}-{mcver}]",
|
||||
mcver = mcversion,
|
||||
forgever = ver,
|
||||
)),
|
||||
})
|
||||
} else {
|
||||
execute!(
|
||||
stdout,
|
||||
SetForegroundColor(Color::Red),
|
||||
Print("Couldn't find forge version for minecraft "),
|
||||
SetAttribute(Attribute::Bold),
|
||||
SetForegroundColor(Color::Cyan),
|
||||
Print(&mcversion),
|
||||
ResetColor,
|
||||
SetForegroundColor(Color::Red),
|
||||
Print(" skipping forge...\n"),
|
||||
ResetColor,
|
||||
)?;
|
||||
}
|
||||
|
||||
tokio::fs::create_dir_all(path).await?;
|
||||
|
||||
let data = serde_json::to_vec_pretty(&Manifest {
|
||||
id: modpack_name,
|
||||
manifest_type: ManifestType::Modpack,
|
||||
versions: vec![Version {
|
||||
version: "1.0".into(),
|
||||
mcversion: vec![mcversion],
|
||||
files: vec![File::Link {
|
||||
id: Some("overrides".into()),
|
||||
link: Link::File("overrides".into()),
|
||||
installer: Installer::Override,
|
||||
options: Some(default_file_opts()),
|
||||
}],
|
||||
relations,
|
||||
}],
|
||||
repositories: vec![Repository {
|
||||
id: "curseforge".into(),
|
||||
repo_type: RepositoryType::Curseforge,
|
||||
url: Url::parse("https://cursemaven.com").unwrap(), // unwrap is ok on fixed value
|
||||
}],
|
||||
})?;
|
||||
|
||||
tokio::fs::write(path.join("modpack.json5"), data).await?;
|
||||
|
||||
stdout.execute(PrintStyledContent(
|
||||
"Created manifest!\n"
|
||||
.with(Color::Green)
|
||||
.attribute(Attribute::Bold),
|
||||
))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
2
mpt/src/commands/mod.rs
Normal file
2
mpt/src/commands/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod downloadmods;
|
||||
pub mod init;
|
22
mpt/src/config.rs
Normal file
22
mpt/src/config.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct Config {
|
||||
pub locations: Locations,
|
||||
pub downloads: Downloads,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Locations {
|
||||
pub src: String,
|
||||
pub temp_dir: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Downloads {
|
||||
pub max_threads: u16,
|
||||
pub http_timeout: u32,
|
||||
}
|
30
mpt/src/forge.rs
Normal file
30
mpt/src/forge.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ForgeVersionResponse {
|
||||
promos: HashMap<String, String>,
|
||||
}
|
||||
|
||||
pub async fn newest_forge_version(
|
||||
http: &Client,
|
||||
mcversion: &str,
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
let resp = http
|
||||
.get("https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json")
|
||||
.send()
|
||||
.await?
|
||||
.bytes()
|
||||
.await?;
|
||||
|
||||
let mut resp = json5::from_str::<ForgeVersionResponse>(std::str::from_utf8(&resp)?)?;
|
||||
|
||||
Ok(resp.promos.remove(&format!("{}-latest", mcversion)))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn parse_version(version: &str) -> Option<&str> {
|
||||
version.split('-').nth(1)
|
||||
}
|
49
mpt/src/main.rs
Normal file
49
mpt/src/main.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
mod commands;
|
||||
mod config;
|
||||
mod forge;
|
||||
mod util;
|
||||
|
||||
#[derive(StructOpt)]
|
||||
struct Opt {
|
||||
#[structopt(subcommand)]
|
||||
cmd: Command,
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
enum Command {
|
||||
Init {
|
||||
#[structopt(help = "The name of the modpack")]
|
||||
modpack_name: String,
|
||||
#[structopt(help = "The minecraft version of the modpack")]
|
||||
mcversion: String,
|
||||
},
|
||||
|
||||
#[structopt(name = "downloadmods")]
|
||||
DownloadMods {
|
||||
#[structopt(help = "Directory to download mods to")]
|
||||
dir: PathBuf,
|
||||
#[structopt(short, long, help = "Download all relations and not just mods")]
|
||||
all: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let Opt { cmd } = Opt::from_args();
|
||||
|
||||
match cmd {
|
||||
Command::Init {
|
||||
modpack_name,
|
||||
mcversion,
|
||||
} => commands::init::run(modpack_name, mcversion).await?,
|
||||
|
||||
Command::DownloadMods { dir, all } => {
|
||||
commands::downloadmods::run(util::parse_config_and_manifest().await?, dir, all).await?
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
82
mpt/src/util.rs
Normal file
82
mpt/src/util.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use addonscript::manifest::{Manifest, Repository};
|
||||
use std::path::Path;
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
pub async fn parse_config() -> anyhow::Result<Config> {
|
||||
let conf = tokio::fs::read("modpacktoolsconfig.toml").await?;
|
||||
Ok(toml::from_slice(&conf)?)
|
||||
}
|
||||
|
||||
pub async fn parse_config_and_manifest() -> anyhow::Result<(Config, Manifest)> {
|
||||
let config = parse_config().await?;
|
||||
let src = Path::new(&config.locations.src);
|
||||
|
||||
let path = if src.join("modpack.json5").exists() {
|
||||
src.join("modpack.json5")
|
||||
} else {
|
||||
src.join("modpack.json")
|
||||
};
|
||||
|
||||
let data = tokio::fs::read(path).await?;
|
||||
let data = std::str::from_utf8(&data)?;
|
||||
let manifest = json5::from_str::<Manifest>(data)?;
|
||||
|
||||
Ok((config, manifest))
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MvnArtifactUrlError {
|
||||
#[error("Maven Artifact specifier has invalid format!")]
|
||||
InvalidFormat,
|
||||
#[error("Url parse error while processing maven artifact: {0}")]
|
||||
UrlParseError(#[from] url::ParseError),
|
||||
}
|
||||
|
||||
pub fn mvn_artifact_to_url(art: &str, repo: &Repository) -> Result<Url, MvnArtifactUrlError> {
|
||||
let mut splits = art.split(':');
|
||||
|
||||
let group_id = splits.next().ok_or(MvnArtifactUrlError::InvalidFormat)?;
|
||||
let artifact_id = splits.next().ok_or(MvnArtifactUrlError::InvalidFormat)?;
|
||||
let version = splits.next().ok_or(MvnArtifactUrlError::InvalidFormat)?;
|
||||
|
||||
let mut url = repo.url.clone();
|
||||
if !url.path().ends_with('/') {
|
||||
url.set_path(&format!("{}/", repo.url.path()));
|
||||
}
|
||||
|
||||
let url = url.join(&format!(
|
||||
"{gid}/{aid}/{v}/{aid}-{v}.jar",
|
||||
gid = group_id.replace('.', "/"),
|
||||
aid = artifact_id,
|
||||
v = version
|
||||
))?;
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use addonscript::manifest::RepositoryType;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn repo() -> Repository {
|
||||
Repository {
|
||||
id: "test".into(),
|
||||
repo_type: RepositoryType::Maven,
|
||||
url: Url::parse("https://example.com/maven").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artifact_to_url_valid() {
|
||||
let res = mvn_artifact_to_url("de.mzte:test:0.1", &repo()).unwrap();
|
||||
assert_eq!(
|
||||
res,
|
||||
Url::parse("https://example.com/maven/de/mzte/test/0.1/test-0.1.jar").unwrap()
|
||||
);
|
||||
}
|
||||
}
|
12
rustfmt.toml
Normal file
12
rustfmt.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
unstable_features = true
|
||||
binop_separator = "Back"
|
||||
format_code_in_doc_comments = true
|
||||
format_macro_matchers = true
|
||||
format_strings = true
|
||||
imports_layout = "HorizontalVertical"
|
||||
match_block_trailing_comma = true
|
||||
merge_imports = true
|
||||
normalize_comments = true
|
||||
use_field_init_shorthand = true
|
||||
use_try_shorthand = true
|
||||
wrap_comments = true
|
Loading…
Reference in a new issue