From f6b4390c3b08e63d9e15f45b1e99b0406a34faee Mon Sep 17 00:00:00 2001 From: TheAlan404 Date: Wed, 19 Jul 2023 23:16:37 +0300 Subject: [PATCH] datapack support and other (fix #9) --- Cargo.lock | 2 +- DOCS.md | 145 ++++++++++++++++++++++++--- README.md | 9 +- TUTORIAL.md | 60 +++++++++--- src/commands/build.rs | 147 ++++++++++++++++++++++------ src/commands/import/customs.rs | 6 +- src/commands/import/datapack.rs | 79 +++++++++++++++ src/commands/import/mod.rs | 5 +- src/commands/import/url.rs | 2 +- src/commands/markdown.rs | 2 +- src/downloadable/import_url.rs | 10 +- src/downloadable/interactive.rs | 35 +++++-- src/downloadable/mod.rs | 5 +- src/downloadable/sources/vanilla.rs | 7 +- src/model/mod.rs | 4 +- src/model/server.rs | 4 + src/model/world.rs | 10 ++ src/util/mod.rs | 8 ++ 18 files changed, 465 insertions(+), 75 deletions(-) create mode 100644 src/commands/import/datapack.rs create mode 100644 src/model/world.rs diff --git a/Cargo.lock b/Cargo.lock index 13e8a1d..77c22f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -786,7 +786,7 @@ checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "mcapi" version = "0.2.0" -source = "git+https://github.com/ParadigmMC/mcapi.git#60058fbebae959fe60481a7489d2ea7bff4e1980" +source = "git+https://github.com/ParadigmMC/mcapi.git#c54cd95036d37e81ae685b4e85a5c1f60806e176" dependencies = [ "os-version", "regex", diff --git a/DOCS.md b/DOCS.md index 6ea30e9..858990c 100644 --- a/DOCS.md +++ b/DOCS.md @@ -1,5 +1,8 @@ + # `mcman` Documentation +You might be looking for [tutorial.md](./TUTORIAL.md) + Index: - [CLI](#cli) @@ -7,6 +10,8 @@ Index: - [Variables](#variables) - [server.toml](#servertoml) - [Server Launcher](#server-launcher) + - [Markdown Options](#markdown-options) + - [World](#world) (for datapacks) - [Downloadables](#downloadable) ## CLI @@ -19,7 +24,7 @@ Initializes a new server in the current directory. This command is interactive. Just run `mcman init`! -The source is the same as one in [`mcman import mrpack`](#mcman-import-mrpack-src) +The mrpack source is the same as one in [`mcman import mrpack`](#mcman-import-mrpack-src) Example using [Adrenaserver](https://modrinth.com/modpack/adrenaserver): @@ -32,21 +37,60 @@ mcman init --mrpack https://cdn.modrinth.com/data/H9OFWiay/versions/2WXUgVhc/Adr ### `mcman version` -Shows the version of mcman. +Show the version and also check for new versions. ### `mcman build` Builds the server into the [output folder](#folder-structure) using the [`server.toml`](#servertoml) and the `config/` directory. -Alternatively set the output folder manually using `--output "outfolder"` option. +
+Extra flags (skip, force) + +You can alternatively set the output folder manually using `--output ` option. + +The `--force` flag can be used to not skip and download everything in the config file. + +You can use the `--skip ` flag to skip stages. + +- Stages should be comma-seperated, like `--skip bootstrap,scripts` +- The stages are: `addons` (plugins and mods), `dp` (datapacks), `bootstrap` (config/) and `scripts` + +
+ +### `mcman pull ` + +'Pulls' a file from `server/` to `config/` + +Example usage: + +```sh +~/smp $ ls + ... + server.toml + ... + +~/smp $ cd server/config/SomeMod -### `mcman readme` +~/smp/server/config/SomeMod $ mcman pull config.txt + server/config/SomeMod/config.txt => config/config/SomeMod/config.txt +``` + +### `mcman info` + +Shows info about the server in the terminal. + +### `mcman markdown` -This command refreshes the server's `README.md` file if there is any. +This command refreshes the markdown files defined in the [server.toml](#markdown-options) files with the templates. -Only the two templates inside the markdown files are refreshed: +**Markdown Templates:** -**Server Info:** This template renders information about the server. +
+ +Server Info Table + + +This template renders a table with server jar info. ```md @@ -60,9 +104,14 @@ Example render: | ------- | ------------------------------------------ | -------- | | 1.20.1 | [Paper](https://papermc.io/software/paper) | *Latest* | ---- +
+ +
+ +Addons List + -**Addons List:** This template renders a list of addons (plugins or mods) +This template renders a list of addons (plugins or mods) ```md @@ -77,7 +126,7 @@ Example render: | [BlueMap](https://modrinth.com/plugin/bluemap) | A Minecraft mapping tool that creates 3D models of your Minecraft worlds and displays them in a web viewer. | | [FastAsyncWorldEdit](https://modrinth.com/plugin/fastasyncworldedit) | Blazingly fast world manipulation for artists, builders and everyone else | ---- +
### `mcman import url ` @@ -98,6 +147,17 @@ mcman import url https://modrinth.com/plugin/imageframe mcman import url https://www.spigotmc.org/resources/armorstandeditor-reborn.94503/ ``` +### `mcman import datapack ` + +Like [import url](#mcman-import-url-url), but imports as a datapack rather than a plugin or a mod. + +Example usage: + +```sh +# datapack alias is dp +mcman import dp https://modrinth.com/plugin/tectonic +``` + ### `mcman import mrpack ` Imports a [mrpack](https://docs.modrinth.com/docs/modpacks/format_definition/) file (modrinth modpacks) @@ -179,7 +239,12 @@ Prefix = "[a]" # key-value table ``` -Or, if your variables are sensitive (such as discord bot tokens) you can use environment variables: +
+ +Using environment variables + + +If your variables are sensitive (such as discord bot tokens) you can use environment variables: ```bash # Linux/Mac: @@ -191,8 +256,17 @@ export TOKEN=asdf set TOKEN=asdf ``` +Environment variables are also put onto config files. + +
+ And then use the variables inside any config file inside `config/`: +
+ +Example configuration files + + 📜 `config/server.properties`: ```properties @@ -218,8 +292,12 @@ messages: token: ${TOKEN} ``` +
+ ### Special Variables +These variables are also present: + - `SERVER_NAME`: name property from server.toml - `SERVER_VERSION`: mc_version property from server.toml @@ -251,6 +329,33 @@ type = "vanilla" # example # ... ``` +**Fields:** + +- `name`: string - Name of the server +- `mc_version`: string | `"latest"` - The minecraft version of the server +- `jar`: [Downloadable](#downloadable) - Which server software to use +- `launcher`: [ServerLauncher](#server-launcher) - Options for generating launch scripts +- `plugins`: [Downloadable[]](#downloadable) - A list of plugins to download +- `mods`: [Downloadable[]](#downloadable) - A list of mods to download +- `variables`: table - More info [here](#variables) +- `worlds`: table - Key is world name in string, value is a [World](#world) +- `markdown`: [MarkdownOptions](#markdown-options) - Options for markdown files + +### World + +> Added in v0.2.2 + +Represents a world in your server. Currently only exists for datapack support. + +```toml +[worlds.skyblock] +datapacks = [] +``` + +**Fields:** + +- `datapacks`: [Downloadable[]](#downloadable) - The list of datapacks to download for this world + ### Server Launcher The `[launcher]` table lets mcman create launch scripts for you. @@ -291,6 +396,24 @@ hello="thing" # jvm_args = "-Dhello=thing" ``` +### Markdown Options + +This category contains the options for markdown rendering via [`mcman md`](#mcman-markdown) + +**Fields:** + +- `files`: string[] - list of files to render +- `auto_update`: bool - weather to auto-update the files on some commands + +```toml +[markdown] +files = [ + "README.md", + "PLUGINS.md", +] +auto_update = false +``` + ## Types Below are some types used in `server.toml` diff --git a/README.md b/README.md index 1a00b5f..a307cf7 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ ![mcman](https://media.discordapp.net/attachments/1109215116060266567/1121117662785851522/mcman_large.png) +[![GitHub release](https://img.shields.io/github/release/ParadigmMC/mcman.svg)](https://github.com/ppy/osu/releases/latest) [![builds](https://img.shields.io/github/actions/workflow/status/ParadigmMC/mcman/build.yml?logo=github)](https://github.com/ParadigmMC/mcman/actions/workflows/build.yml) [![docker publish](https://img.shields.io/github/actions/workflow/status/ParadigmMC/mcman/publish.yml?logo=github&label=docker%20publish)](https://github.com/ParadigmMC/mcman/actions/workflows/publish.yml) ![GitHub Repo stars](https://img.shields.io/github/stars/ParadigmMC/mcman?logo=github) @@ -25,7 +26,8 @@ Powerful Minecraft Server Manager CLI. Easily install jars (server, plugins & mo - Velocity - Waterfall - BungeeCord - - Plugins/Mods: + - Spigot and CraftBukkit + - Plugins/Mods/Datapacks: - Modrinth - Spigot - And even **Github Releases**, **Custom URL**s and **Jenkins!** @@ -47,12 +49,17 @@ Powerful Minecraft Server Manager CLI. Easily install jars (server, plugins & mo - 📋 Want an example? Here's [iptfreedom](https://github.com/IPTFreedom/iptfreedom) +Submit a PR or open an issue if you have a mcman-server repository that we can add here! + ## Changelog ### `0.2.2` (unreleased) +- Added support for **Datapacks** + - Added command `mcman import datapack` - Added **BuildTools** support. - This includes *spigot, bukkit and craftbukkit* +- Even better docs and tutorial.md ### `0.2.1` diff --git a/TUTORIAL.md b/TUTORIAL.md index 4695d84..40b6750 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1,26 +1,24 @@ -[latest-win]: https://github.com/ParadigmMC/mcman/releases/download/latest/mcman.exe -[latest-linux]: https://github.com/ParadigmMC/mcman/releases/download/latest/mcman +[latest-win]: https://github.com/ParadigmMC/mcman/releases/latest/download/mcman.exe +[latest-linux]: https://github.com/ParadigmMC/mcman/releases/latest/download/mcman # Getting Started -## Installation - -**Stable Releases:** +**Index:** -| Windows | OSX/Linux | -| :------------------: | :--------------------: | -| [latest][latest-win] | [latest][latest-linux] | +- [Installation](#installation) +- [Recommended Usage](#recommended-usage) +- [Using configuration files](#using-configuration-files) -For past releases, go to the [releases](https://github.com/ParadigmMC/mcman/releases) tab. - -**Dev Releases:** +## Installation -We have github [actions](https://github.com/ParadigmMC/mcman/actions/workflows/build.yml) that build mcman. +**Stable Releases:** -These require you to be logged in to github. +| [Windows][latest-win] | [OSX/Linux][latest-linux] | +| :-------------------: | :-----------------------: | -Please note that these builds might not work completely. +- [Github Releases](https://github.com/ParadigmMC/mcman/releases) +- [build action](https://github.com/ParadigmMC/mcman/actions/workflows/build.yml) (requires github account) ## Recommended Usage @@ -80,3 +78,37 @@ mcman build && cd server && start ## Usage with Docker After initialization mcman also provides a default dockerfile for you, this dockerfile basically runs your server after running `mcman build`. + +## Using configuration files + +While running a minecraft server, 99% of the time you have to edit the configuration files of the server. **mcman** can actually help you with that. + +Next to your `server.toml` file, you'll see a `config/` folder. It's actually pretty simple. **mcman** will copy files from `config/` to `server/` while building your server (after installing the server, plugins/mods/datapacks etc.) + +Actually, mcman doesn't *just* copy the files. It has **variables** too - you can use variables inside your configuration files. This is very useful if you want to have something (like the server name) in multiple places - if you want to change it, you can just change the variable in `server.toml` (instead of manually going through every file that contains it) + +> Note +> `config/server.properties` should already be present after mcman init, so pretend it doesn't exist for now + +For an example, let's create a `server.properties` file in `config/` and fill it like so: + +```properties +motd=${MESSAGE} +``` + +And add this to `server.toml`: + +```toml +[variables] +MESSAGE = "Hello from server.toml!" +``` + +After you run `mcman build`, you can see that `server/server.properties` is like this: + +```properties +motd=Hello from server.toml! +``` + +You can read more about [variables](./DOCS.md#variables) here. + +**Tip:** You can 'pull' a config file from `server/` to `config/` with the [`mcman pull`](./DOCS.md#mcman-pull-file) command. diff --git a/src/commands/build.rs b/src/commands/build.rs index 698d807..1acdd89 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -29,24 +29,27 @@ pub fn cli() -> Command { arg!(-o --output [FILE] "The output directory for the server") .value_parser(value_parser!(PathBuf)), ) - .arg( - arg!(--skip [stages] "Skip some stages") - .value_delimiter(',') - .default_value(""), - ) + .arg(arg!(--skip [stages] "Skip some stages").value_delimiter(',')) + .arg(arg!(--force "Don't skip downloading already downloaded jars")) } #[allow(clippy::if_not_else)] +#[allow(clippy::too_many_lines)] pub async fn run(matches: &ArgMatches) -> Result<()> { let server = Server::load().context("Failed to load server.toml")?; let http_client = reqwest::Client::builder() .user_agent(APP_USER_AGENT) .build() .context("Failed to create HTTP client")?; + let default_output = server.path.join("server"); - let output_dir = matches.get_one::("output").unwrap_or(&default_output); + let output_dir = matches + .get_one::("output") + .unwrap_or(&default_output); std::fs::create_dir_all(output_dir).context("Failed to create output directory")?; + let force = matches.get_flag("force"); + //let term = Term::stdout(); let title = Style::new().blue().bold(); @@ -56,8 +59,16 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { let skip_stages = matches .get_many::("skip") - .unwrap() - .collect::>(); + .map(|o| o.cloned().collect::>()) + .unwrap_or(vec![]); + + if force { + println!(" => {}", style("Force flag used").bold()); + } + + if !skip_stages.is_empty() { + println!(" => skipping stages: {}", skip_stages.join(", ")); + } let mut stage_index = 1; @@ -73,15 +84,15 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { // stage 1: server jar mark_stage("Server Jar"); - let serverjar_name = download_server_jar(&server, &http_client, output_dir) + let serverjar_name = download_server_jar(&server, &http_client, output_dir, force) .await .context("Failed to download server jar")?; // stage 2: plugins - if !skip_stages.contains(&&"addons".to_owned()) { + if !skip_stages.contains(&"addons".to_owned()) { if !server.plugins.is_empty() { mark_stage("Plugins"); - download_addons("plugins", &server, &http_client, output_dir) + download_addons("plugins", &server, &http_client, output_dir, force) .await .context("Failed to download plugins")?; } @@ -89,7 +100,7 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { // stage 3: mods if !server.mods.is_empty() { mark_stage("Mods"); - download_addons("mods", &server, &http_client, output_dir) + download_addons("mods", &server, &http_client, output_dir, force) .await .context("Failed to download plugins")?; } @@ -97,10 +108,20 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { mark_stage_skipped("addons"); } + if !server.worlds.is_empty() { + if !skip_stages.contains(&"dp".to_owned()) { + mark_stage("Datapacks"); + + download_datapacks(&server, &http_client, output_dir, force).await?; + } else { + mark_stage_skipped("datapacks"); + } + } + // stage 4: bootstrap mark_stage("Configurations"); - if !skip_stages.contains(&&"bootstrap".to_owned()) && server.path.join("config").exists() { + if !skip_stages.contains(&"bootstrap".to_owned()) && server.path.join("config").exists() { let mut vars = HashMap::new(); for (key, value) in &server.variables { @@ -126,7 +147,7 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { match server.jar { Downloadable::Quilt { .. } | Downloadable::Fabric { .. } - | Downloadable::Vanilla { } => { + | Downloadable::Vanilla {} => { println!( " {}", style("=> eula.txt [eula_args unsupported]").dim() @@ -143,7 +164,7 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { } // stage 5: launcher scripts - if !skip_stages.contains(&&"scripts".to_owned()) { + if !skip_stages.contains(&"scripts".to_owned()) { if !server.launcher.disable { mark_stage("Scripts"); create_scripts(&server, &serverjar_name, output_dir)?; @@ -161,6 +182,75 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { Ok(()) } +async fn download_datapacks( + server: &Server, + http_client: &reqwest::Client, + output_dir: &Path, + force: bool, +) -> Result<()> { + let world_count = server.worlds.len(); + let wc_len = world_count.to_string().len(); + + for (idx, (name, world)) in server.worlds.iter().enumerate() { + println!(" ({idx:wc_len$}/{world_count}) World: {name}"); + + std::fs::create_dir_all(output_dir.join(name).join("datapacks")) + .context(format!("Failed to create {name}/datapacks directory"))?; + + let datapack_count = world.datapacks.len(); + let dp_len = datapack_count.to_string().len(); + let pad_len = wc_len * 2 + 4; + + for (idx, dp) in world.datapacks.iter().enumerate() { + let dp_name = dp.get_filename(server, http_client).await?; + if !force + && output_dir + .join(name) + .join("datapacks") + .join(&dp_name) + .exists() + { + println!( + " {:pad_len$}({:dp_len$}/{}) Skipping : {}", + "", + idx, + datapack_count, + style(&dp_name).dim() + ); + continue; + } + + util::download_with_progress( + File::create( + &output_dir + .join(name) + .join("datapacks") + .join(dp_name.clone()), + ) + .await + .context(format!("Failed to create output file for {dp_name}"))?, + &dp_name, + dp, + server, + http_client, + ) + .await + .context(format!("Failed to download plugin {dp_name}"))?; + + println!( + " {:pad_len$}({}/{}) {} : {}", + "", + idx, + datapack_count, + style("Downloaded").green().bold(), + style(&dp_name).dim() + ); + } + } + + Ok(()) +} + async fn execute_child( cmd: &mut std::process::Command, output_dir: &Path, @@ -215,11 +305,12 @@ async fn download_server_jar( server: &Server, http_client: &reqwest::Client, output_dir: &Path, + force: bool, ) -> Result { let serverjar_name = match &server.jar { Downloadable::Quilt { loader, .. } => { let installerjar_name = server.jar.get_filename(server, http_client).await?; - if output_dir.join(installerjar_name.clone()).exists() { + if !force && output_dir.join(installerjar_name.clone()).exists() { println!( " Quilt installer present ({})", style(installerjar_name.clone()).dim() @@ -247,10 +338,12 @@ async fn download_server_jar( .await .context("getting loader version id")?; - let serverjar_name = - format!("quilt-server-launch-{}-{}.jar", server.mc_version, loader_id); + let serverjar_name = format!( + "quilt-server-launch-{}-{}.jar", + server.mc_version, loader_id + ); - if output_dir.join(serverjar_name.clone()).exists() { + if !force && output_dir.join(serverjar_name.clone()).exists() { println!( " Skipping server jar ({})", style(serverjar_name.clone()).dim() @@ -303,7 +396,7 @@ async fn download_server_jar( } Downloadable::BuildTools { args } => { let installerjar_name = server.jar.get_filename(server, http_client).await?; - if output_dir.join(installerjar_name.clone()).exists() { + if !force && output_dir.join(installerjar_name.clone()).exists() { println!( " BuildTools present ({})", style(installerjar_name.clone()).dim() @@ -329,7 +422,7 @@ async fn download_server_jar( let serverjar_name = format!("spigot-{}.jar", server.mc_version); - if output_dir.join(serverjar_name.clone()).exists() { + if !force && output_dir.join(serverjar_name.clone()).exists() { println!( " Skipping server jar ({})", style(serverjar_name.clone()).dim() @@ -337,12 +430,7 @@ async fn download_server_jar( } else { println!(" Running BuildTools...",); - let mut exec_args = vec![ - "-jar", - &installerjar_name, - "--rev", - &server.mc_version, - ]; + let mut exec_args = vec!["-jar", &installerjar_name, "--rev", &server.mc_version]; for arg in args { exec_args.push(arg); @@ -375,7 +463,7 @@ async fn download_server_jar( } dl => { let serverjar_name = dl.get_filename(server, http_client).await?; - if output_dir.join(serverjar_name.clone()).exists() { + if !force && output_dir.join(serverjar_name.clone()).exists() { println!( " Skipping server jar ({})", style(serverjar_name.clone()).dim() @@ -411,6 +499,7 @@ async fn download_addons( server: &Server, http_client: &reqwest::Client, output_dir: &Path, + force: bool, ) -> Result<()> { let addon_count = match addon_type { "plugins" => server.plugins.len(), @@ -435,7 +524,7 @@ async fn download_addons( i += 1; let addon_name = addon.get_filename(server, http_client).await?; - if output_dir.join(addon_type).join(&addon_name).exists() { + if !force && output_dir.join(addon_type).join(&addon_name).exists() { println!( " ({}/{}) Skipping : {}", i, diff --git a/src/commands/import/customs.rs b/src/commands/import/customs.rs index f19defd..05b3b09 100644 --- a/src/commands/import/customs.rs +++ b/src/commands/import/customs.rs @@ -24,7 +24,8 @@ pub async fn run() -> Result<()> { Downloadable::Url { url, .. } => { println!(" > {}", style("Re-importing:").cyan().bold()); println!(" {}", style(&url).dim()); - if let Ok(d) = Downloadable::from_url_interactive(&http_client, &server, url, false).await + if let Ok(d) = + Downloadable::from_url_interactive(&http_client, &server, url, false).await { d } else { @@ -41,7 +42,8 @@ pub async fn run() -> Result<()> { Downloadable::Url { url, .. } => { println!(" > {}", style("Re-importing:").cyan().bold()); println!(" {url}"); - if let Ok(d) = Downloadable::from_url_interactive(&http_client, &server, url, false).await + if let Ok(d) = + Downloadable::from_url_interactive(&http_client, &server, url, false).await { d } else { diff --git a/src/commands/import/datapack.rs b/src/commands/import/datapack.rs new file mode 100644 index 0000000..39cca85 --- /dev/null +++ b/src/commands/import/datapack.rs @@ -0,0 +1,79 @@ +use anyhow::{Context, Result}; +use clap::{arg, ArgMatches, Command}; +use dialoguer::{theme::ColorfulTheme, Input, Select}; + +use crate::{ + commands::version::APP_USER_AGENT, + downloadable::Downloadable, + model::{Server, World}, + util::SelectItem, +}; + +pub fn cli() -> Command { + Command::new("datapack") + .about("Import datapack from url") + .visible_alias("dp") + .arg(arg!().required(false)) +} + +pub async fn run(matches: &ArgMatches) -> Result<()> { + let mut server = Server::load().context("Failed to load server.toml")?; + + let http_client = reqwest::Client::builder() + .user_agent(APP_USER_AGENT) + .build() + .context("Failed to create HTTP client")?; + + let urlstr = match matches.get_one::("url") { + Some(url) => url.clone(), + None => Input::::new().with_prompt("URL:").interact_text()?, + }; + + let dl = Downloadable::from_url_interactive(&http_client, &server, &urlstr, true).await?; + + let selected_world_name = if server.worlds.is_empty() { + "*".to_owned() + } else { + let mut items: Vec> = server + .worlds + .keys() + .map(|k| SelectItem(k.clone(), k.clone())) + .collect(); + + items.push(SelectItem("*".to_owned(), "* New world entry".to_owned())); + + let idx = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Which world to add to?") + .items(&items) + .default(items.len() - 1) + .interact()?; + + items[idx].0.clone() + }; + + let world_name = if selected_world_name == "*" { + Input::with_theme(&ColorfulTheme::default()) + .with_prompt("World name?") + .default("world".to_owned()) + .interact_text()? + } else { + selected_world_name + }; + + if !server.worlds.contains_key(&world_name) { + server.worlds.insert(world_name.clone(), World::default()); + } + + server + .worlds + .get_mut(&world_name) + .expect("world shouldve already been inserted") + .datapacks + .push(dl); + + server.save()?; + + println!(" > Datapack added to {world_name}!"); + + Ok(()) +} diff --git a/src/commands/import/mod.rs b/src/commands/import/mod.rs index 3c43043..8df631b 100644 --- a/src/commands/import/mod.rs +++ b/src/commands/import/mod.rs @@ -2,6 +2,7 @@ use anyhow::Result; use clap::{ArgMatches, Command}; mod customs; +mod datapack; mod mrpack; mod url; @@ -12,13 +13,15 @@ pub fn cli() -> Command { .subcommand_required(true) .arg_required_else_help(true) .subcommand(url::cli()) - .subcommand(customs::cli()) + .subcommand(datapack::cli()) .subcommand(mrpack::cli()) + .subcommand(customs::cli()) } pub async fn run(matches: &ArgMatches) -> Result<()> { match matches.subcommand() { Some(("url", sub_matches)) => url::run(sub_matches).await?, + Some(("datapack" | "dp", sub_matches)) => datapack::run(sub_matches).await?, Some(("mrpack", sub_matches)) => mrpack::run(sub_matches).await?, Some(("customs", _)) => customs::run().await?, _ => unreachable!(), diff --git a/src/commands/import/url.rs b/src/commands/import/url.rs index d1037fc..eddd611 100644 --- a/src/commands/import/url.rs +++ b/src/commands/import/url.rs @@ -20,7 +20,7 @@ pub async fn run(matches: &ArgMatches) -> Result<()> { let urlstr = match matches.get_one::("url") { Some(url) => url.clone(), - None => Input::::new().with_prompt("URL:").interact()?, + None => Input::::new().with_prompt("URL:").interact_text()?, }; let addon = Downloadable::from_url_interactive(&http_client, &server, &urlstr, false).await?; diff --git a/src/commands/markdown.rs b/src/commands/markdown.rs index f0eff4e..cca5c5f 100644 --- a/src/commands/markdown.rs +++ b/src/commands/markdown.rs @@ -162,7 +162,7 @@ pub fn initialize_readme(server: &Server) -> Result<()> { "Mods" } else { "Plugins" - } + }, ); f.write_all(readme_content.as_bytes())?; diff --git a/src/downloadable/import_url.rs b/src/downloadable/import_url.rs index e195cfd..507eebb 100644 --- a/src/downloadable/import_url.rs +++ b/src/downloadable/import_url.rs @@ -68,10 +68,12 @@ impl Downloadable { .into_iter() // TODO: better filtering, commented out because proxy server versioning is complex..? //.filter(|v| v.game_versions.contains(&server.mc_version)) - .filter(|v| if datapack_mode { - v.loaders.contains(&"datapack".to_owned()) - } else { - !v.loaders.contains(&"datapack".to_owned()) + .filter(|v| { + if datapack_mode { + v.loaders.contains(&"datapack".to_owned()) + } else { + !v.loaders.contains(&"datapack".to_owned()) + } }) .collect(); diff --git a/src/downloadable/interactive.rs b/src/downloadable/interactive.rs index 3e0b5f8..9d33e77 100644 --- a/src/downloadable/interactive.rs +++ b/src/downloadable/interactive.rs @@ -1,22 +1,26 @@ use anyhow::{bail, Result}; use dialoguer::{theme::ColorfulTheme, Select}; +use crate::util::SelectItem; + use super::Downloadable; impl Downloadable { pub fn select_jar_interactive() -> Result { let items = vec![ - (0, "Vanilla - No patches"), - (1, "PaperMC/Paper - Spigot fork, most popular"), - (2, "Purpur - Paper fork"), + SelectItem(0, "Vanilla - No patches".to_owned()), + SelectItem(1, "PaperMC/Paper - Spigot fork, most popular".to_owned()), + SelectItem(2, "Purpur - Paper fork".to_owned()), + SelectItem( + 3, + "BuildTools - Spigot, Bukkit or CraftBukkit".to_owned(), + ), ]; - let items_str: Vec = items.iter().map(|v| v.1.to_owned()).collect(); - let jar_type = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Which server software to use?") .default(0) - .items(&items_str) + .items(&items) .interact()?; Ok(match jar_type { @@ -25,6 +29,25 @@ impl Downloadable { 2 => Self::Purpur { build: "latest".to_owned(), }, + 3 => { + let items = vec![ + SelectItem(Self::BuildTools { args: vec![] }, "Spigot".to_owned()), + SelectItem( + Self::BuildTools { + args: vec!["--compile".to_owned(), "craftbukkit".to_owned()], + }, + "CraftBukkit".to_owned(), + ), + ]; + + let idx = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Which one?") + .default(0) + .items(&items) + .interact()?; + + items[idx].0.clone() + } _ => unreachable!(), }) } diff --git a/src/downloadable/mod.rs b/src/downloadable/mod.rs index 6067c56..a82ba33 100644 --- a/src/downloadable/mod.rs +++ b/src/downloadable/mod.rs @@ -297,7 +297,10 @@ impl Downloadable { } pub fn is_modded(&self) -> bool { - matches!(self, Downloadable::Fabric { .. } | Downloadable::Quilt { .. }) + matches!( + self, + Downloadable::Fabric { .. } | Downloadable::Quilt { .. } + ) } } diff --git a/src/downloadable/sources/vanilla.rs b/src/downloadable/sources/vanilla.rs index 494243d..f566acc 100644 --- a/src/downloadable/sources/vanilla.rs +++ b/src/downloadable/sources/vanilla.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; pub async fn fetch_vanilla(version: &str, client: &reqwest::Client) -> Result { let version_manifest = mcapi::vanilla::fetch_version_manifest(client).await?; @@ -9,7 +9,10 @@ pub async fn fetch_vanilla(version: &str, client: &reqwest::Client) -> Result version_manifest.fetch(id, client).await?, } .downloads - .server + .get(&mcapi::vanilla::DownloadType::Server) + .ok_or(anyhow!( + "version manifest doesn't include a server download" + ))? .download(client) .await?) } diff --git a/src/model/mod.rs b/src/model/mod.rs index 53e7c19..01085d2 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,2 +1,4 @@ mod server; -pub use server::{MarkdownOptions, Server, ServerLauncher}; +mod world; +pub use server::*; +pub use world::*; diff --git a/src/model/server.rs b/src/model/server.rs index d764cec..06f0449 100644 --- a/src/model/server.rs +++ b/src/model/server.rs @@ -11,6 +11,8 @@ use serde::{Deserialize, Serialize}; use crate::downloadable::Downloadable; +use super::World; + #[derive(Debug, Deserialize, Serialize)] #[serde(default)] pub struct ServerLauncher { @@ -143,6 +145,7 @@ pub struct Server { pub plugins: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] pub mods: Vec, + pub worlds: HashMap, pub markdown: Option, } @@ -209,6 +212,7 @@ impl Default for Server { launcher: ServerLauncher::default(), plugins: vec![], mods: vec![], + worlds: HashMap::new(), markdown: Some(MarkdownOptions::default()), } } diff --git a/src/model/world.rs b/src/model/world.rs new file mode 100644 index 0000000..01c0bc5 --- /dev/null +++ b/src/model/world.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +use crate::downloadable::Downloadable; + +#[derive(Debug, Deserialize, Serialize, Default)] +#[serde(default)] +pub struct World { + #[serde(skip_serializing_if = "Vec::is_empty")] + pub datapacks: Vec, +} diff --git a/src/util/mod.rs b/src/util/mod.rs index bf19b16..70263f9 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -8,6 +8,14 @@ use tokio::{fs::File, io::BufWriter}; use crate::{downloadable::Downloadable, model::Server}; +pub struct SelectItem(pub T, pub String); + +impl ToString for SelectItem { + fn to_string(&self) -> String { + self.1.clone() + } +} + pub async fn download_with_progress( file: File, message: &str,