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,