From 12254440664cbca54e07ab677adeae824ae5d5b5 Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Tue, 14 Nov 2023 22:23:37 +0300 Subject: [PATCH] it is blanketcon ready --- .github/workflows/bc23test.yml | 12 ++ README.md | 35 ++-- TODO.md | 39 ---- examples/network/network.toml | 5 +- examples/network/servers/game1/README.md | 13 ++ .../servers/game1/config/server.properties | 2 + examples/network/servers/game1/server.toml | 18 ++ src/app/caching.rs | 8 +- src/app/downloading.rs | 4 +- src/app/feedback.rs | 23 +++ src/app/from_string.rs | 116 ++++++++++- src/app/mod.rs | 19 ++ src/commands/markdown.rs | 22 --- src/commands/pull.rs | 4 + src/core/addons.rs | 20 +- src/core/mod.rs | 2 +- src/core/scripts.rs | 36 +++- src/core/serverjar.rs | 120 ++++++++++-- src/hot_reload/mod.rs | 5 +- src/interop/mrpack.rs | 8 +- src/interop/packwiz.rs | 15 +- src/model/serverlauncher.rs | 4 +- src/model/servertoml/mod.rs | 23 +++ src/model/servertype/mod.rs | 183 ++---------------- src/sources/curserinth.rs | 6 +- src/sources/github.rs | 12 +- src/sources/hangar.rs | 39 ++-- src/sources/maven.rs | 130 +++++++++++-- src/sources/modrinth.rs | 22 ++- src/sources/purpur.rs | 9 +- src/sources/quilt.rs | 1 + 31 files changed, 622 insertions(+), 333 deletions(-) create mode 100644 .github/workflows/bc23test.yml delete mode 100644 TODO.md create mode 100644 examples/network/servers/game1/README.md create mode 100644 examples/network/servers/game1/config/server.properties create mode 100644 examples/network/servers/game1/server.toml diff --git a/.github/workflows/bc23test.yml b/.github/workflows/bc23test.yml new file mode 100644 index 0000000..919b155 --- /dev/null +++ b/.github/workflows/bc23test.yml @@ -0,0 +1,12 @@ +on: + workflow_dispatch + +name: Test bc23 +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: rustup toolchain install stable + - uses: Swatinem/rust-cache@v2 + - run: cargo run --quiet run --test diff --git a/README.md b/README.md index 3ca65f7..7d75530 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ![downloads](https://img.shields.io/github/downloads/ParadigmMC/mcman/total?logo=github) [![discord server](https://img.shields.io/discord/1108817072814817410?logo=discord)](https://discord.gg/YFuxNmKvSr) -Powerful Minecraft Server Manager CLI. Easily install jars (server, plugins & mods) and write config files. Docker and git support included. +Powerful Minecraft Server Manager CLI. Easily install jars (server, plugins & mods), write config files, manage worlds and more. Docker and git support included. ## Getting Started @@ -20,7 +20,7 @@ Powerful Minecraft Server Manager CLI. Easily install jars (server, plugins & mo | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------ | - Join the [discord](https://discord.gg/YFuxNmKvSr) for support! -- 📋 Want an example? Here's [iptfreedom](https://github.com/IPTFreedom/iptfreedom) +- 📋 Some examples can be found under [examples/](./examples/) Submit a PR or open an issue if you have a mcman-server repository that we can add here! @@ -32,15 +32,29 @@ Submit a PR or open an issue if you have a mcman-server repository that we can a - ✔️ No third-party hosts (metadata/mirrors) - :octocat: Fully `git`-compatible! - 🐳 Supports Docker, out of the box -- 📦 Import from or export to [mrpack](./DOCS.md#mcman-import-mrpack-src)s! -- 📦 Import from or export to [packwiz](./DOCS.md#mcman-import-mrpack-src) packs! +- 📦 **Import** from or **export** to `mrpack`s or `packwiz` packs! - 📚 Supports way too many sources, some are: - Modrinth, CurseRinth, Spigot, Hangar, Github Releases, Jenkins, Maven - If you need something else, it even supports custom urls! - ⚙️ Better configuration files with `config/`! - Allows you to use **variables** inside your config files - Use *environment variables* for secrets -- 🛡️ More secure than in-game plugin managers which are prone to permission attacks +- 🌐 Keep worlds as `worlds/*.zip` for git, or set it to be downloaded from somewhere! +- ✨ Managing a network? Use `network.toml` to manage servers' ports, have shared variables, config files, plugins or mods. + +## Reviews + +"faster than gradle builds" + +\- kuylar + +"makes even oracle linux usable" + +\- PureComedi + +"I'm technically a contributor" + +\- Trash Panda ## Changelog @@ -48,16 +62,7 @@ whats a semver? /s ### `0.4.0` -- Added [NeoForge](https://neoforged.net/) server type -- Added [Forge](https://forums.minecraftforge.net/) server type -- Added Downloadable type **Maven** -- Added [Hangar](https://hangar.papermc.io/) support -- Improved building process -- Implemented a lockfile to speed things up and fix the remove-issue -- Fixed a bug on `mcman run` which messed up the output when server crashes -- Some downloadables now support variables -- Experimental `add` command -- Experimental `world` feature +- See [#31](https://github.com/ParadigmMC/mcman/issues/31) ### `0.3.0` diff --git a/TODO.md b/TODO.md deleted file mode 100644 index d67d812..0000000 --- a/TODO.md +++ /dev/null @@ -1,39 +0,0 @@ -# secret todo list lol - -## sources - -- [ ] Forge -- [ ] Hangar -- [ ] Bukkit/Curseforge -- [ ] File - -## features - -- [ ] datapacks -- [ ] packwiz integration -- [ ] export as mrpack - -## commands - -- [x] mcman init --name -- [x] mcman build - - [x] server jar - - [x] plugins - - [x] bootstrap files - - [x] launcher scripts -- [ ] mcman edit -- [ ] mcman plugins add \ \ [output] -- [ ] mcman plugins remove \ -- [ ] ??? mcman plugins search \ \ -- [ ] mcman plugins update \ -- [ ] mcman set server \ [data] -- [ ] mcman set version \ - -## other stuff - -- [x] vars - - [x] toml - - [x] env -- [x] support git-like calls from inner folders (recurse server.toml lookup) -- [x] mods support (along with plugins) -- [x] server readme lol diff --git a/examples/network/network.toml b/examples/network/network.toml index 936a4ee..7c803e1 100644 --- a/examples/network/network.toml +++ b/examples/network/network.toml @@ -2,8 +2,11 @@ name = "CoolNetwork" proxy = "proxy" port = 25565 +[servers.game1] +port = 25567 + [servers.lobby] port = 25566 -groups = [] [variables] +MADE_USING = "mcman" diff --git a/examples/network/servers/game1/README.md b/examples/network/servers/game1/README.md new file mode 100644 index 0000000..0fb3368 --- /dev/null +++ b/examples/network/servers/game1/README.md @@ -0,0 +1,13 @@ +# game1 + +[![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) + + + + + + +## Mods + + + diff --git a/examples/network/servers/game1/config/server.properties b/examples/network/servers/game1/config/server.properties new file mode 100644 index 0000000..675815e --- /dev/null +++ b/examples/network/servers/game1/config/server.properties @@ -0,0 +1,2 @@ +server-port=${PORT:25565} +motd=${NW_MADE_USING} diff --git a/examples/network/servers/game1/server.toml b/examples/network/servers/game1/server.toml new file mode 100644 index 0000000..da1411e --- /dev/null +++ b/examples/network/servers/game1/server.toml @@ -0,0 +1,18 @@ +name = "game1" +mc_version = "1.20.2" + +[jar] +type = "quilt" +loader = "latest" +installer = "latest" + +[variables] +PORT = "25565" + +[launcher] +nogui = true +preset_flags = "aikars" +eula_args = true + +[options] +upload_to_mclogs = false diff --git a/src/app/caching.rs b/src/app/caching.rs index 6f58172..6190cec 100644 --- a/src/app/caching.rs +++ b/src/app/caching.rs @@ -1,6 +1,6 @@ use std::{path::{PathBuf, Path}, fs::{self, File}, io::Write}; -use anyhow::Result; +use anyhow::{Result, Context}; use serde::de::DeserializeOwned; pub struct Cache(pub PathBuf); @@ -37,9 +37,11 @@ impl Cache { } pub fn write_json(&self, path: &str, data: &T) -> Result<()> { - fs::create_dir_all(PathBuf::from(path).parent().unwrap_or(Path::new(path)))?; + fs::create_dir_all(self.path(path).parent().unwrap()) + .context(format!("Creating parent directory for: {path}"))?; let content = serde_json::to_string(data)?; - let mut f = File::create(self.0.join(path))?; + let mut f = File::create(self.path(path)) + .context(format!("Creating cache file at: {path}"))?; f.write_all(content.as_bytes())?; Ok(()) diff --git a/src/app/downloading.rs b/src/app/downloading.rs index 410dc13..63c4482 100644 --- a/src/app/downloading.rs +++ b/src/app/downloading.rs @@ -150,7 +150,7 @@ impl App { // file already there and is ok self.log(format!( " {} {}", - style("Skipped ").green(), + style(" Skipped").green(), progress_bar.message() ))?; @@ -218,7 +218,7 @@ impl App { validate_hash(hasher)?; progress_bar.set_style(ProgressStyle::with_template("{prefix:.green.bold} {msg}")?); - progress_bar.set_prefix("Copied "); + progress_bar.set_prefix(" Copied"); progress_bar.set_message(resolved.filename.clone()); self.log(format!( " {} {}", diff --git a/src/app/feedback.rs b/src/app/feedback.rs index ce7eb62..c2cf8e5 100644 --- a/src/app/feedback.rs +++ b/src/app/feedback.rs @@ -14,6 +14,13 @@ impl App { ))?) } + pub fn error(&self, message: S) -> Result<()> { + Ok(self.multi_progress.println(format!( + " {} {message}", + style("⚠ Error").red().bold() + ))?) + } + pub fn success(&self, message: S) -> Result<()> { Ok(self.multi_progress.suspend(|| println!( " {} {message}", @@ -37,6 +44,10 @@ impl App { ))) } + pub fn println(&self, message: S) -> Result<()> { + Ok(self.multi_progress.suspend(|| println!("{message}"))) + } + pub fn dbg(&self, message: S) -> Result<()> { Ok(self.multi_progress.suspend(|| println!( " {} {}", @@ -106,4 +117,16 @@ impl App { Ok(item.0.clone()) } + + pub fn select_with_default(&self, prompt: &str, items: &[SelectItem], def: usize) -> Result { + let item = &items[self.multi_progress.suspend(|| { + Select::with_theme(&ColorfulTheme::default()) + .items(items) + .with_prompt(prompt) + .default(def) + .interact() + })?]; + + Ok(item.0.clone()) + } } diff --git a/src/app/from_string.rs b/src/app/from_string.rs index d4b4170..af41daf 100644 --- a/src/app/from_string.rs +++ b/src/app/from_string.rs @@ -1,4 +1,4 @@ -use anyhow::{Result, bail}; +use anyhow::{Result, bail, Context}; use crate::{model::Downloadable, util::SelectItem}; @@ -10,7 +10,7 @@ impl App { s: &str ) -> Result { if s.starts_with("http") { - Ok(self.dl_from_url(s).await?) + Ok(self.dl_from_url(s).await.context(format!("Importing URL: {s}"))?) } else if s.contains(':') { match s.split_once(':').unwrap() { ("mr" | "modrinth", id) => { @@ -61,6 +61,21 @@ impl App { &self, urlstr: &str ) -> Result { + let urlstring = if urlstr.starts_with("https://blanketcon.b-cdn.net") { + let mut path = urlstr.strip_prefix("https://blanketcon.b-cdn.net/").unwrap().split('/'); + match path.next() { + Some("devos") => format!("https://mvn.devos.one/{}", path.collect::>().join("/")), + Some("jared") => format!("https://ci.blamejared.com/{}", path.collect::>().join("/")), + Some("jaskarth") => format!("https://jaskarth.com/{}", path.collect::>().join("/")), + Some("ithundxr") => format!("https://maven.ithundxr.dev/{}", path.collect::>().join("/")), + Some("pub") => urlstr.to_owned(), + _ => format!("https://github.com/{}", urlstr.strip_prefix("https://blanketcon.b-cdn.net/").unwrap()), + } + } else { + urlstr.to_owned() + }; + + let urlstr = &urlstring; let url = reqwest::Url::parse(urlstr)?; match (url.domain(), url.path_segments().map(|x| x.collect::>()).unwrap_or_default().as_slice()) { @@ -238,11 +253,24 @@ impl App { Ok(Downloadable::GithubRelease { repo, tag: tag.to_string(), asset }) } - _ => { - let selection = self.select(&urlstr, &vec![ + (domain, path) => { + let def = match domain { + Some(d) if d.starts_with("ci.") => 1, + Some(d) if d.starts_with("maven.") || d.starts_with("mvn") => 2, + _ => { + if path.ends_with(&["maven-metadata.xml"]) { + 2 + } else { + 0 + } + } + }; + + let selection = self.select_with_default(&urlstr, &vec![ SelectItem(0, "Add as Custom URL".to_owned()), SelectItem(1, "Add as Jenkins".to_owned()), - ])?; + SelectItem(2, "Add as Maven".to_owned()), + ], def)?; match selection { 0 => { @@ -307,6 +335,84 @@ impl App { artifact, }) } + 2 => { + let mut repo = None; + let mut group_id = None; + let mut artifact_id = None; + let mut ver = None; + + if let Ok(meta) = self.maven().find_maven_artifact(urlstr).await { + if let Some((inferred_repo, rest)) = meta.find_url(urlstr) { + if self.confirm(&format!("Is '{inferred_repo}' the maven repository url?"))? { + repo = Some(inferred_repo); + } + + ver = if !rest.is_empty() { + rest.split('/').next() + } else { None } + } + group_id = meta.group_id; + artifact_id = meta.artifact_id; + } + + let repo = match repo { + Some(r) => r, + None => self.prompt_string_filled("Maven repository url?", urlstr)?, + }; + + let group = match group_id { + Some(r) => r, + None => { + let inferred = if urlstr.starts_with(&repo) { + let p = urlstr.strip_prefix(&repo).unwrap(); + if p.ends_with(".jar") { + let mut li = p.rsplit('/').skip(2).collect::>(); + li.reverse(); + li + } else { + p.split('/').collect::>() + }.into_iter().filter(|x| !x.is_empty()).collect::>().join(".") + } else { + "".to_string() + }; + + self.prompt_string_filled("Group (split by .)?", &inferred)? + }, + }; + + let suggest = format!("{repo}/{}", group.replace('.', "/")); + + let artifact = match artifact_id { + Some(r) => r, + None => self.prompt_string_filled("Artifact?", if urlstr.starts_with(&suggest) { + urlstr.strip_prefix(&suggest).unwrap().split('/').filter(|x| !x.is_empty()).next().unwrap_or("") + } else { + "" + })?, + }; + + let mut versions = vec![SelectItem("latest".to_owned(), "Always use latest".to_owned())]; + + for v in self.maven().fetch_metadata(&repo, &group, &artifact).await?.versions { + versions.push(SelectItem(v.clone(), v.clone())); + } + + let version = self.select("Which version?", &versions)?; + + let filename = if urlstr.ends_with(".jar") { + urlstr.rsplit('/').next().unwrap().to_owned() + } else { + self.prompt_string_default("Filename?", "${artifact}-${version}.jar")? + }; + + Ok(Downloadable::Maven { + url: repo, + group, + artifact, + version, + filename + }) + } _ => unreachable!(), } } diff --git a/src/app/mod.rs b/src/app/mod.rs index a22acd0..228bef8 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -98,6 +98,24 @@ impl App { match k { "SERVER_NAME" => Some(self.server.name.clone()), "SERVER_VERSION" | "mcver" | "mcversion" => Some(self.server.mc_version.clone()), + + "SERVER_PORT" => { + std::env::var(format!("PORT_{}", self.server.name)) + .ok() + .or(self.network + .as_ref() + .and_then(|nw| nw.servers.get(&self.server.name)) + .map(|s| s.port.to_string())) + }, + "SERVER_IP" => { + std::env::var(format!("IP_{}", self.server.name)) + .ok() + .or(self.network + .as_ref() + .and_then(|nw| nw.servers.get(&self.server.name)) + .and_then(|s| s.ip_address.clone())) + }, + "PLUGIN_COUNT" => Some(self.server.plugins.len().to_string()), "MOD_COUNT" => Some(self.server.mods.len().to_string()), "WORLD_COUNT" => Some(self.server.worlds.len().to_string()), @@ -105,6 +123,7 @@ impl App { "NETWORK_NAME" => Some(self.network.as_ref()?.name.clone()), "NETWORK_PORT" => Some(self.network.as_ref()?.port.to_string()), + "NETWORK_SERVERS_COUNT" => Some(self.network.as_ref()?.servers.len().to_string()), "NETWORK_VELOCITY_SERVERS" => { if let Some(nw) = &self.network { diff --git a/src/commands/markdown.rs b/src/commands/markdown.rs index 130626c..8030aa9 100644 --- a/src/commands/markdown.rs +++ b/src/commands/markdown.rs @@ -1,11 +1,8 @@ use console::style; use dialoguer::theme::ColorfulTheme; use dialoguer::Confirm; -use std::fs::File; -use std::io::Write; use crate::app::App; -use crate::model::Server; use anyhow::Result; pub async fn run(mut app: App) -> Result<()> { @@ -27,22 +24,3 @@ pub async fn run(mut app: App) -> Result<()> { Ok(()) } - -pub fn initialize_readme(server: &Server) -> Result<()> { - let mut f = File::create("./README.md")?; - let readme_content = include_str!("../../res/default_readme"); - let readme_content = readme_content - .replace("{SERVER_NAME}", &server.name) - .replace( - "{ADDON_HEADER}", - if server.jar.is_modded() { - "Mods" - } else { - "Plugins" - }, - ); - - f.write_all(readme_content.as_bytes())?; - - Ok(()) -} diff --git a/src/commands/pull.rs b/src/commands/pull.rs index 79a47eb..955ded9 100644 --- a/src/commands/pull.rs +++ b/src/commands/pull.rs @@ -86,5 +86,9 @@ pub fn run(app: App, args: Args) -> Result<()> { style("config/").bold(), )); + if skipped != 0 { + app.warn(format!("Skipped {skipped} files"))?; + } + Ok(()) } diff --git a/src/core/addons.rs b/src/core/addons.rs index 6f722e4..e6f56eb 100644 --- a/src/core/addons.rs +++ b/src/core/addons.rs @@ -1,6 +1,6 @@ use std::{collections::HashSet, time::Duration}; -use anyhow::Result; +use anyhow::{Result, bail}; use indicatif::{ProgressIterator, ProgressBar, ProgressStyle, FormattedDuration}; use tokio::fs; @@ -34,7 +34,23 @@ impl<'a> BuildContext<'a> { .with_message(format!("Processing {addon_type}s")); let pb = self.app.multi_progress.add(pb); for addon in server_list.iter().progress_with(pb.clone()) { - let (_path, resolved) = self.downloadable(addon, &addon_type.folder(), Some(&pb)).await?; + let mut attempt = 0; + let max_tries = std::env::var("MAX_TRIES") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(3); + let (_path, resolved) = loop { + match self.downloadable(addon, &addon_type.folder(), Some(&pb)).await { + Ok(d) => break d, + Err(e) => { + self.app.error(e)?; + if max_tries > attempt { + bail!("Max attempts reached"); + } + attempt += 1; + } + } + }; files_list.insert(resolved.filename.clone()); diff --git a/src/core/mod.rs b/src/core/mod.rs index 95d4f1b..fa53b5c 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -68,7 +68,7 @@ impl<'a> BuildContext<'a> { self.bootstrap_files().await?; if !self.app.server.launcher.disable { - let startup = self.app.server.jar.get_startup_method(&self.app, &server_jar).await?; + let startup = self.get_startup_method(&server_jar).await?; self.create_scripts(startup).await?; diff --git a/src/core/scripts.rs b/src/core/scripts.rs index 1ef8497..53f2d19 100644 --- a/src/core/scripts.rs +++ b/src/core/scripts.rs @@ -3,11 +3,45 @@ use std::{fs::OpenOptions, io::Write}; use anyhow::Result; use tokio::fs; -use crate::model::StartupMethod; +use crate::model::{StartupMethod, ServerType}; use super::BuildContext; impl<'a> BuildContext<'a> { + pub async fn get_startup_method( + &self, + serverjar_name: &str, + ) -> Result { + let mcver = &self.app.mc_version(); + Ok(match &self.app.server.jar { + ServerType::NeoForge { loader } => { + let l = self.app.neoforge().resolve_version(loader).await?; + + StartupMethod::Custom { + windows: vec![format!( + "@libraries/net/neoforged/forge/{mcver}-{l}/win_args.txt" + )], + linux: vec![format!( + "@libraries/net/neoforged/forge/{mcver}-{l}/unix_args.txt" + )], + } + } + ServerType::Forge { loader } => { + let l = self.app.forge().resolve_version(loader).await?; + + StartupMethod::Custom { + windows: vec![format!( + "@libraries/net/minecraftforge/forge/{mcver}-{l}/win_args.txt" + )], + linux: vec![format!( + "@libraries/net/minecraftforge/forge/{mcver}-{l}/unix_args.txt" + )], + } + } + _ => StartupMethod::Jar(serverjar_name.to_owned()), + }) + } + pub async fn create_scripts(&self, startup: StartupMethod) -> Result<()> { fs::write( self.output_dir.join("start.bat"), diff --git a/src/core/serverjar.rs b/src/core/serverjar.rs index fdc5f11..59e614a 100644 --- a/src/core/serverjar.rs +++ b/src/core/serverjar.rs @@ -12,19 +12,93 @@ use tokio::{ io::AsyncWriteExt, }; -use crate::model::InstallMethod; +use crate::{model::{InstallMethod, ServerType}, sources::quilt}; use super::BuildContext; impl<'a> BuildContext<'a> { + pub async fn get_install_method( + &self + ) -> Result { + let mcver = &self.app.mc_version(); + Ok(match self.app.server.jar.clone() { + ServerType::Quilt { loader, .. } => { + let mut args = vec!["install", "server", mcver]; + + if loader != "latest" { + args.push(&loader); + } + + args.push("--install-dir=."); + args.push("--download-server"); + + InstallMethod::Installer { + name: "Quilt Server Installer".to_owned(), + label: "qsi".to_owned(), + args: args.into_iter().map(ToOwned::to_owned).collect(), + rename_from: Some("quilt-server-launch.jar".to_owned()), + jar_name: format!( + "quilt-server-launch-{mcver}-{}.jar", + quilt::map_quilt_loader_version(&self.app.http_client, &loader) + .await + .context("resolving quilt loader version id (latest/latest-beta)")? + ), + } + } + ServerType::NeoForge { loader } => InstallMethod::Installer { + name: "NeoForged Installer".to_owned(), + label: "nfi".to_owned(), + args: vec!["--installServer".to_owned(), ".".to_owned()], + rename_from: None, + jar_name: format!( + "libraries/net/neoforged/forge/{mcver}-{0}/forge-{mcver}-{0}-server.jar", + self.app.neoforge().resolve_version(&loader).await? + ) + }, + ServerType::Forge { loader } => InstallMethod::Installer { + name: "Forge Installer".to_owned(), + label: "fi".to_owned(), + args: vec!["--installServer".to_owned(), ".".to_owned()], + rename_from: None, + jar_name: format!( + "libraries/net/minecraftforge/forge/{mcver}-{0}/forge-{mcver}-{0}-server.jar", + self.app.forge().resolve_version(&loader).await? + ) + }, + ServerType::BuildTools { args, software } => { + let mut buildtools_args = vec![ + "--compile", + &software, + "--compile-if-changed", + "--rev", + mcver, + ]; + + for arg in &args { + buildtools_args.push(arg); + } + + InstallMethod::Installer { + name: "BuildTools".to_owned(), + label: "bt".to_owned(), + args: buildtools_args.into_iter().map(ToOwned::to_owned).collect(), + rename_from: Some("server.jar".to_owned()), + jar_name: format!( + "{}-{mcver}.jar", + if software == "craftbukkit" { + "craftbukkit" + } else { + "spigot" + } + ), + } + } + _ => InstallMethod::SingleJar, + }) + } + pub async fn download_server_jar(&'a self) -> Result { - let serverjar_name = match self - .app - .server - .jar - .get_install_method(&self.app) - .await? - { + let serverjar_name = match self.get_install_method().await? { InstallMethod::Installer { name, label, @@ -72,20 +146,32 @@ impl<'a> BuildContext<'a> { cmd_args.push(arg); } - self.execute_child(("java", cmd_args.clone()), &name, &label) + let java = std::env::var("JAVA_BIN").unwrap_or("java".to_owned()); + + self.execute_child((&java, cmd_args.clone()), &name, &label) .await .context(format!("Executing command: 'java {}'", cmd_args.join(" "))) .context(format!("Running installer: {name}"))?; if let Some(from) = &rename_from { - pb.set_message(format!( - "Renaming... ({})", - style(format!("{from} => {jar_name}")).dim() - )); - - fs::rename(self.output_dir.join(from), self.output_dir.join(&jar_name)) - .await - .context(format!("Renaming: {from} => {jar_name}"))?; + let from_path = self.output_dir.join(from); + let to_path = self.output_dir.join(&jar_name); + if from_path.exists() { + pb.set_message(format!( + "Renaming... ({})", + style(format!("{from} => {jar_name}")).dim() + )); + + fs::rename(from_path, &to_path) + .await + .context(format!("Renaming: {from} => {jar_name}"))?; + } { + if to_path.exists() { + self.app.log(format!(" Rename skipped ({from} doesn't exist)"))?; + } else { + bail!("Installer did not output '{from}', can't rename to '{jar_name}'"); + } + } } self.app.log( diff --git a/src/hot_reload/mod.rs b/src/hot_reload/mod.rs index 82dec68..d70d243 100644 --- a/src/hot_reload/mod.rs +++ b/src/hot_reload/mod.rs @@ -61,7 +61,7 @@ pub enum TestResult { // [x] commands are not being sent properly // [x] use debouncer for notify // [ ] reload server.toml properly -// [ ] tests +// [x] tests impl<'a> DevSession<'a> { pub async fn spawn_child(&mut self) -> Result { @@ -76,8 +76,7 @@ impl<'a> DevSession<'a> { .args( self.builder.app.server .launcher - .get_arguments(&self.builder.app.server.jar.get_startup_method( - &self.builder.app, + .get_arguments(&self.builder.get_startup_method( &self.jar_name.as_ref().unwrap().clone() ).await?, platform), ) diff --git a/src/interop/mrpack.rs b/src/interop/mrpack.rs index 3bb9aed..4fe2b3d 100644 --- a/src/interop/mrpack.rs +++ b/src/interop/mrpack.rs @@ -29,6 +29,8 @@ impl<'a> MRPackInterop<'a> { let index = mrpack.read_index()?; + self.0.server.fill_from_map(&index.dependencies); + progress_bar.set_style(ProgressStyle::with_template("{prefix:.blue.bold} {msg} [{wide_bar:.cyan/blue}] {pos}/{len}")?); progress_bar.set_prefix("Importing mod"); for file in index.files.iter().progress_with(progress_bar.clone()) { @@ -59,7 +61,7 @@ impl<'a> MRPackInterop<'a> { std::fs::create_dir_all(target_path.parent().unwrap())?; - // TODO is target_path exists prompt + // TODO mrpack import: is target_path exists prompt let pb = self.0.multi_progress.insert_after(&progress_bar, ProgressBar::new(zip_file.size())); @@ -135,7 +137,7 @@ impl<'a> MRPackInterop<'a> { mrpack.write_index(&index)?; pb.set_prefix("Overrides"); - // TODO + // TODO: mrpack export overrides pb.finish(); mrpack.finish()?; @@ -154,7 +156,7 @@ impl<'a> MRPackInterop<'a> { Ok(MRPackFile { path: format!("mods/{}", resolved.filename), hashes: resolved.hashes, - // TODO: EnvSupport + // TODO: mrpack export EnvSupport env: None, downloads: vec![resolved.url] }) diff --git a/src/interop/packwiz.rs b/src/interop/packwiz.rs index 8b3b2da..2f8d273 100644 --- a/src/interop/packwiz.rs +++ b/src/interop/packwiz.rs @@ -1,6 +1,7 @@ use std::{path::PathBuf, time::Duration, collections::HashMap, str::FromStr}; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Result, Context}; +use console::style; use indicatif::{ProgressBar, ProgressFinish, ProgressStyle, ProgressIterator}; use rpackwiz::model::{Mod, ModUpdate, ModDownload, HashFormat, DownloadMode, Pack, PackIndex}; use serde::de::DeserializeOwned; @@ -64,6 +65,8 @@ impl<'a> PackwizInterop<'a> { progress_bar.enable_steady_tick(Duration::from_millis(250)); let pack: Pack = source.parse_toml("pack.toml").await?; + + self.0.server.fill_from_map(&pack.versions); progress_bar.set_message("Reading pack index..."); @@ -82,6 +85,11 @@ impl<'a> PackwizInterop<'a> { let modpw: Mod = source.parse_toml(&file.file).await?; let dl = self.from_mod(&modpw).await?; + self.0.println(format!( + "{} {}", + style(" Imported").green().bold(), + dl.to_short_string() + ))?; self.0.server.mods.push(dl); } else { @@ -133,11 +141,12 @@ impl<'a> PackwizInterop<'a> { } else if let Some(dl) = self.from_mod_update(&m.update)? { Ok(dl) } else { - self.0.dl_from_url(&m.download + self.0.dl_from_string(&m.download .url .clone() - .ok_or(anyhow!("download url not present"))?) + .ok_or(anyhow!("Download URL not present for mod: {m:#?}"))?) .await + .context(format!("Importing mod: {m:#?}")) } } diff --git a/src/model/serverlauncher.rs b/src/model/serverlauncher.rs index cbd1c1a..51a1453 100644 --- a/src/model/serverlauncher.rs +++ b/src/model/serverlauncher.rs @@ -62,10 +62,10 @@ pub enum StartupMethod { impl ServerLauncher { pub fn get_java(&self) -> String { - if let Some(Some(path)) = self.java_version.as_ref().map(|v| std::env::var(format!("JAVA_{v}")).ok()) { + if let Some(Some(path)) = self.java_version.as_ref().map(|v| std::env::var(format!("JAVA_{v}_BIN")).ok()) { path } else { - String::from("java") + std::env::var("JAVA_BIN").unwrap_or(String::from("java")) } } diff --git a/src/model/servertoml/mod.rs b/src/model/servertoml/mod.rs index af25d9d..e84b419 100644 --- a/src/model/servertoml/mod.rs +++ b/src/model/servertoml/mod.rs @@ -107,6 +107,29 @@ impl Server { }) } + pub fn fill_from_map(&mut self, map: &HashMap) { + if let Some(v) = map.get("minecraft") { + self.mc_version = v.clone(); + } + + if let Some(v) = map.get("forge") { + self.jar = ServerType::Forge { loader: v.clone() } + } + + if let Some(v) = map.get("neoforge") { + self.jar = ServerType::NeoForge { loader: v.clone() } + } + + if let Some(v) = map.get("fabric-loader").or(map.get("fabric")) { + self.jar = ServerType::Fabric { loader: v.clone(), installer: "latest".to_owned() } + } + + if let Some(v) = map.get("quilt-loader").or(map.get("quilt")) { + self.jar = ServerType::Quilt { loader: v.clone(), installer: "latest".to_owned() } + } + } + + // TODO: move to ModrinthAPI pub fn filter_modrinth_versions(&self, list: &[modrinth::ModrinthVersion]) -> Vec { let is_proxy = self.jar.get_software_type() == SoftwareType::Proxy; let is_vanilla = matches!(self.jar, ServerType::Vanilla {}); diff --git a/src/model/servertype/mod.rs b/src/model/servertype/mod.rs index dbabb18..d5c7950 100644 --- a/src/model/servertype/mod.rs +++ b/src/model/servertype/mod.rs @@ -116,184 +116,23 @@ impl ServerType { } } - // TODO: move this to somewhere else, like BuildContext - pub async fn get_install_method( - &self, - app: &App, - ) -> Result { - let mcver = &app.server.mc_version; - Ok(match self.clone() { - Self::Quilt { loader, .. } => { - let mut args = vec!["install", "server", mcver]; - - if loader != "latest" { - args.push(&loader); - } - - args.push("--install-dir=."); - args.push("--download-server"); - - InstallMethod::Installer { - name: "Quilt Server Installer".to_owned(), - label: "qsi".to_owned(), - args: args.into_iter().map(ToOwned::to_owned).collect(), - rename_from: Some("quilt-server-launch.jar".to_owned()), - jar_name: format!( - "quilt-server-launch-{mcver}-{}.jar", - quilt::map_quilt_loader_version(&app.http_client, &loader) - .await - .context("resolving quilt loader version id (latest/latest-beta)")? - ), - } - } - Self::NeoForge { loader } => InstallMethod::Installer { - name: "NeoForged Installer".to_owned(), - label: "nfi".to_owned(), - args: vec!["--installServer".to_owned(), ".".to_owned()], - rename_from: None, - jar_name: format!( - "libraries/net/neoforged/forge/{mcver}-{0}/forge-{mcver}-{0}-server.jar", - app.neoforge().resolve_version(&loader).await? - ) - }, - Self::Forge { loader } => InstallMethod::Installer { - name: "Forge Installer".to_owned(), - label: "fi".to_owned(), - args: vec!["--installServer".to_owned(), ".".to_owned()], - rename_from: None, - jar_name: format!( - "libraries/net/minecraftforge/forge/{mcver}-{0}/forge-{mcver}-{0}-server.jar", - app.forge().resolve_version(&loader).await? - ) - }, - Self::BuildTools { args, software } => { - let mut buildtools_args = vec![ - "--compile", - &software, - "--compile-if-changed", - "--rev", - mcver, - ]; - - for arg in &args { - buildtools_args.push(arg); - } - - InstallMethod::Installer { - name: "BuildTools".to_owned(), - label: "bt".to_owned(), - args: buildtools_args.into_iter().map(ToOwned::to_owned).collect(), - rename_from: Some("server.jar".to_owned()), - jar_name: format!( - "{}-{mcver}.jar", - if software == "craftbukkit" { - "craftbukkit" - } else { - "spigot" - } - ), - } - } - _ => InstallMethod::SingleJar, - }) - } - - // TODO: move this to somewhere else, like BuildContext - pub async fn get_startup_method( - &self, - app: &App, - serverjar_name: &str, - ) -> Result { - let mcver = &app.server.mc_version; - Ok(match self { - Self::NeoForge { loader } => { - let l = app.neoforge().resolve_version(loader).await?; - - StartupMethod::Custom { - windows: vec![format!( - "@libraries/net/neoforged/forge/{mcver}-{l}/win_args.txt" - )], - linux: vec![format!( - "@libraries/net/neoforged/forge/{mcver}-{l}/unix_args.txt" - )], - } - } - Self::Forge { loader } => { - let l = app.forge().resolve_version(loader).await?; - - StartupMethod::Custom { - windows: vec![format!( - "@libraries/net/minecraftforge/forge/{mcver}-{l}/win_args.txt" - )], - linux: vec![format!( - "@libraries/net/minecraftforge/forge/{mcver}-{l}/unix_args.txt" - )], - } - } - _ => StartupMethod::Jar(serverjar_name.to_owned()), - }) - } - - // TODO: move to ModrinthAPI - pub fn get_modrinth_facets(&self, mcver: &str) -> Result { - let mut arr: Vec> = vec![]; - - if self.get_software_type() != SoftwareType::Proxy { - arr.push(vec![format!("versions:{}", mcver.to_owned())]); - } - - if let Some(n) = self.get_modrinth_name() { - arr.push(vec![format!("categories:{n}")]); - } - - Ok(serde_json::to_string(&arr)?) - } - - // TODO: move to ModrinthAPI pub fn get_modrinth_name(&self) -> Option { match self { - Self::Fabric { .. } => Some("fabric"), - Self::Quilt { .. } => Some("quilt"), - Self::Forge { .. } => Some("forge"), - Self::NeoForge { .. } => Some("neoforge"), - Self::Paper { } => Some("paper"), - Self::BuildTools { .. } => Some("spigot"), - Self::Purpur { .. } => Some("purpur"), - Self::BungeeCord { } => Some("bungeecord"), - Self::Velocity { } => Some("velocity"), - Self::Waterfall { } => Some("waterfall"), - Self::PaperMC { project, .. } => Some(project.as_str()), + ServerType::Fabric { .. } => Some("fabric"), + ServerType::Quilt { .. } => Some("quilt"), + ServerType::Forge { .. } => Some("forge"), + ServerType::NeoForge { .. } => Some("neoforge"), + ServerType::Paper { } => Some("paper"), + ServerType::BuildTools { .. } => Some("spigot"), + ServerType::Purpur { .. } => Some("purpur"), + ServerType::BungeeCord { } => Some("bungeecord"), + ServerType::Velocity { } => Some("velocity"), + ServerType::Waterfall { } => Some("waterfall"), + ServerType::PaperMC { project, .. } => Some(project.as_str()), _ => None, }.map(|o| o.to_owned()) } - // TODO: move to HangarAPI - pub fn get_hangar_platform(&self) -> Option { - match self { - Self::Waterfall {} => Some(mcapi::hangar::Platform::Waterfall), - Self::Velocity {} => Some(mcapi::hangar::Platform::Velocity), - Self::PaperMC { project, .. } if project == "waterfall" => Some(mcapi::hangar::Platform::Waterfall), - Self::PaperMC { project, .. } if project == "velocity" => Some(mcapi::hangar::Platform::Velocity), - Self::PaperMC { project, .. } if project == "paper" => Some(mcapi::hangar::Platform::Paper), - Self::Paper { } | Self::Purpur { .. } => Some(mcapi::hangar::Platform::Paper), - _ => None - } - } - - // TODO: move to HangarAPI - pub fn get_hangar_versions_filter(&self, mcver: &str) -> mcapi::hangar::VersionsFilter { - let platform = self.get_hangar_platform(); - mcapi::hangar::VersionsFilter { - platform_version: if platform.is_some() { - Some(mcver.to_owned()) - } else { - None - }, - platform, - ..Default::default() - } - } - pub fn is_modded(&self) -> bool { self.get_software_type() == SoftwareType::Modded } diff --git a/src/sources/curserinth.rs b/src/sources/curserinth.rs index 2876d42..875f3dd 100644 --- a/src/sources/curserinth.rs +++ b/src/sources/curserinth.rs @@ -70,12 +70,16 @@ impl<'a> CurserinthAPI<'a> { self.fetch_api(format!("{CURSERINTH_API}/project/{id}/version")).await } + pub fn get_modrinth_name(&self) -> Option { + self.0.server.jar.get_modrinth_name() + } + /// Result<(filtered, unfiltered)> pub async fn fetch_versions(&self, id: &str) -> Result<(Vec, Vec)> { let versions = self.fetch_all_versions(id).await?; Ok(( - versions.iter().filter(|v| if let Some(loader) = self.0.server.jar.get_modrinth_name() { + versions.iter().filter(|v| if let Some(loader) = self.get_modrinth_name() { v.loaders.contains(&loader) } else { true diff --git a/src/sources/github.rs b/src/sources/github.rs index b7708bc..9ff9f47 100644 --- a/src/sources/github.rs +++ b/src/sources/github.rs @@ -52,7 +52,7 @@ impl GithubWaitRatelimit for reqwest::Response { #[derive(Debug, Deserialize, Serialize, Clone)] pub struct CachedData { pub data: T, - pub last_modified: String, + pub etag: String, } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -100,7 +100,7 @@ impl<'a> GithubAPI<'a> { .with_token(None) // TODO: token via App .headers(if let Some(cached_data) = &cached_data { let mut map = HeaderMap::new(); - map.insert("If-Modified-Since", HeaderValue::from_str(&cached_data.last_modified)?); + map.insert("if-none-match", HeaderValue::from_str(&cached_data.etag)?); map } else { HeaderMap::new() @@ -109,10 +109,9 @@ impl<'a> GithubAPI<'a> { .await?; if response.status() == StatusCode::NOT_MODIFIED { - self.0.dbg(format!("GithubAPI: Cache hit: {cache_path}"))?; Ok(cached_data.unwrap().data) } else { - let last_modified = response.headers().get("Last-Modified").cloned(); + let etag = response.headers().get("etag").cloned(); let json: T = response .error_for_status()? @@ -121,13 +120,12 @@ impl<'a> GithubAPI<'a> { .json() .await?; - if let Some(last_modified) = last_modified { + if let Some(etag) = etag { if let Some(cache) = self.0.get_cache(CACHE_DIR) { cache.write_json(&cache_path, &CachedData { - last_modified: last_modified.to_str()?.to_owned(), + etag: etag.to_str()?.to_owned(), data: json.clone(), }).context("Saving github api response to cache")?; - self.0.dbg(format!("GithubAPI: Saved: {cache_path}"))?; } } diff --git a/src/sources/hangar.rs b/src/sources/hangar.rs index f66a328..e04f589 100644 --- a/src/sources/hangar.rs +++ b/src/sources/hangar.rs @@ -3,17 +3,13 @@ use std::collections::HashMap; use anyhow::{anyhow, Context, Result}; use mcapi::hangar::{Platform, ProjectVersion}; -use crate::app::{App, CacheStrategy, ResolvedFile}; +use crate::{app::{App, CacheStrategy, ResolvedFile}, model::ServerType}; pub struct HangarAPI<'a>(pub &'a App); impl<'a> HangarAPI<'a> { pub async fn fetch_hangar_version(&self, id: &str, version: &str) -> Result { - let filter = self - .0 - .server - .jar - .get_hangar_versions_filter(&self.0.server.mc_version); + let filter = self.get_versions_filter(); let version = if version == "latest" { let versions = @@ -55,6 +51,31 @@ impl<'a> HangarAPI<'a> { Ok(version) } + pub fn get_platform(&self) -> Option { + match &self.0.server.jar { + ServerType::Waterfall {} => Some(mcapi::hangar::Platform::Waterfall), + ServerType::Velocity {} => Some(mcapi::hangar::Platform::Velocity), + ServerType::PaperMC { project, .. } if project == "waterfall" => Some(mcapi::hangar::Platform::Waterfall), + ServerType::PaperMC { project, .. } if project == "velocity" => Some(mcapi::hangar::Platform::Velocity), + ServerType::PaperMC { project, .. } if project == "paper" => Some(mcapi::hangar::Platform::Paper), + ServerType::Paper { } | ServerType::Purpur { .. } => Some(mcapi::hangar::Platform::Paper), + _ => None + } + } + + pub fn get_versions_filter(&self) -> mcapi::hangar::VersionsFilter { + let platform = self.get_platform(); + mcapi::hangar::VersionsFilter { + platform_version: if platform.is_some() { + Some(self.0.mc_version()) + } else { + None + }, + platform, + ..Default::default() + } + } + pub async fn resolve_source(&self, id: &str, version: &str) -> Result { let version = self .fetch_hangar_version(id, version) @@ -64,11 +85,7 @@ impl<'a> HangarAPI<'a> { let download = version .downloads .get( - &self - .0 - .server - .jar - .get_hangar_platform() + &self.get_platform() .unwrap_or(Platform::Paper), ) .ok_or(anyhow!( diff --git a/src/sources/maven.rs b/src/sources/maven.rs index 85915c9..d6b9c72 100644 --- a/src/sources/maven.rs +++ b/src/sources/maven.rs @@ -4,6 +4,55 @@ use anyhow::{anyhow, Result}; use crate::app::{App, ResolvedFile, CacheStrategy}; +pub trait XMLExt { + fn get_text(&self, k: &str) -> Result; + fn get_text_all(&self, k: &str) -> Vec; +} + +impl XMLExt for roxmltree::Document<'_> { + fn get_text(&self, k: &str) -> Result { + Ok(self + .descendants() + .find_map(|elem| if elem.tag_name().name() == k { + Some(elem.text()?.to_owned()) + } else { + None + }).ok_or(anyhow!("XML element not found: {}", k))? + ) + } + + fn get_text_all(&self, k: &str) -> Vec { + self.descendants() + .filter_map(|t| { + if t.tag_name().name() == "version" { + Some(t.text()?.to_owned()) + } else { + None + } + }) + .collect::>() + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct MavenMetadata { + pub latest: Option, + pub group_id: Option, + pub artifact_id: Option, + pub versions: Vec, +} + +impl MavenMetadata { + pub fn find_url(&self, url: &str) -> Option<(String, String)> { + let t = url.split_once(&format!( + "{}/{}", + self.group_id.clone()?.replace('.', "/"), + self.artifact_id.clone()? + ))?; + Some((t.0.to_owned(), t.1.to_owned())) + } +} + pub struct MavenAPI<'a>(pub &'a App); impl<'a> MavenAPI<'a> { @@ -14,6 +63,68 @@ impl<'a> MavenAPI<'a> { ) } + pub async fn find_maven_artifact(&self, url: &str) -> Result { + let metadata_url = if url.ends_with("/maven-metadata.xml") { + url.to_string() + } else { + self.guess_metadata_url(url)? + }; + + self.fetch_metadata_url(&metadata_url).await + } + + // @author ChatGPT + pub fn guess_metadata_url(&self, url: &str) -> Result { + // Attempt to construct the Maven metadata URL based on the provided URL + let segments: Vec<&str> = url.trim_end_matches('/').rsplit('/').collect(); + + if let Some(last_segment) = segments.first() { + if last_segment.is_empty() { + // If the last segment is empty, skip it + let metadata_url = format!("{}/maven-metadata.xml", url.trim_end_matches('/')); + return Ok(metadata_url); + } + } + + if segments.len() >= 2 { + // Construct the Maven metadata URL by going up one level + let metadata_url = format!("{}/maven-metadata.xml", url.trim_end_matches(segments[0]).trim_end_matches('/')); + Ok(metadata_url) + } else { + Err(anyhow!("Invalid URL format")) + } + } + + pub async fn fetch_metadata( + &self, + url: &str, + group_id: &str, + artifact_id: &str, + ) -> Result { + self.fetch_metadata_url(&Self::get_metadata_url(url, group_id, artifact_id)).await + } + + pub async fn fetch_metadata_url( + &self, + url: &str, + ) -> Result { + let xml = self.0.http_client + .get(url) + .send() + .await? + .text() + .await?; + + let doc = roxmltree::Document::parse(&xml)?; + + Ok(MavenMetadata { + latest: doc.get_text("latest").ok(), + artifact_id: doc.get_text("artifactId").ok(), + group_id: doc.get_text("groupId").ok(), + versions: doc.get_text_all("version"), + }) + } + pub async fn fetch_versions( &self, url: &str, @@ -29,24 +140,9 @@ impl<'a> MavenAPI<'a> { let doc = roxmltree::Document::parse(&xml)?; - let latest = doc.descendants().find_map(|t| { - if t.tag_name().name() == "latest" { - Some(t.text()?.to_owned()) - } else { - None - } - }); + let latest = doc.get_text("latest").ok(); - let list = doc - .descendants() - .filter_map(|t| { - if t.tag_name().name() == "version" { - Some(t.text()?.to_owned()) - } else { - None - } - }) - .collect::>(); + let list = doc.get_text_all("version"); Ok(( latest.unwrap_or_else(|| list.first().cloned().unwrap_or_default()), diff --git a/src/sources/modrinth.rs b/src/sources/modrinth.rs index 4ed5fcb..e3ae8fd 100644 --- a/src/sources/modrinth.rs +++ b/src/sources/modrinth.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use anyhow::{Result, anyhow}; use serde::{Deserialize, Serialize, de::DeserializeOwned}; -use crate::app::{App, ResolvedFile, CacheStrategy}; +use crate::{app::{App, ResolvedFile, CacheStrategy}, model::{ServerType, SoftwareType}}; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct ModrinthProject { @@ -159,9 +159,27 @@ impl<'a> ModrinthAPI<'a> { )) } + pub fn get_modrinth_name(&self) -> Option { + self.0.server.jar.get_modrinth_name() + } + + pub fn get_modrinth_facets(&self) -> String { + let mut arr: Vec> = vec![]; + + if self.0.server.jar.get_software_type() != SoftwareType::Proxy { + arr.push(vec![format!("versions:{}", self.0.mc_version())]); + } + + if let Some(n) = self.get_modrinth_name() { + arr.push(vec![format!("categories:{n}")]); + } + + serde_json::to_string(&arr).unwrap() + } + pub async fn search(&self, query: &str) -> Result> { Ok(self.0.http_client.get(format!("{API_URL}/search")) - .query(&[("query", query), ("facets", &self.0.server.jar.get_modrinth_facets(&self.0.mc_version())?)]) + .query(&[("query", query), ("facets", &self.get_modrinth_facets())]) .send() .await? .error_for_status()? diff --git a/src/sources/purpur.rs b/src/sources/purpur.rs index 6784204..7c3cdd6 100644 --- a/src/sources/purpur.rs +++ b/src/sources/purpur.rs @@ -23,6 +23,7 @@ impl<'a> PurpurAPI<'a> { Ok(response) } + #[allow(unused)] pub async fn fetch_versions(&self) -> Result> { Ok(self.fetch_api::(API_URL).await?.versions) } @@ -61,26 +62,26 @@ impl<'a> PurpurAPI<'a> { } #[derive(Debug, Deserialize, Serialize, Clone)] -struct PurpurMCResponse { +pub struct PurpurMCResponse { pub project: String, pub versions: Vec, } #[derive(Debug, Deserialize, Serialize, Clone)] -struct PurpurMCVersion { +pub struct PurpurMCVersion { pub builds: PurpurMCBuilds, pub project: String, pub version: String, } #[derive(Debug, Deserialize, Serialize, Clone)] -struct PurpurMCBuilds { +pub struct PurpurMCBuilds { pub latest: PurpurMCBuild, pub all: Vec, } #[derive(Debug, Deserialize, Serialize, Clone)] -struct PurpurMCBuild { +pub struct PurpurMCBuild { pub project: String, pub version: String, pub build: String, diff --git a/src/sources/quilt.rs b/src/sources/quilt.rs index 2a53478..4415771 100644 --- a/src/sources/quilt.rs +++ b/src/sources/quilt.rs @@ -25,6 +25,7 @@ impl<'a> QuiltAPI<'a> { } } +// TODO ... pub async fn map_quilt_loader_version(client: &reqwest::Client, loader: &str) -> Result { Ok(match loader { "latest" => mcapi::quilt::fetch_loaders(client)