diff --git a/.gitattibutes b/.gitattibutes new file mode 100644 index 0000000..8580f63 --- /dev/null +++ b/.gitattibutes @@ -0,0 +1,3 @@ + +# mcman: use lfs for worlds +**/worlds/*.zip filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1b17c2c..b55ba1c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ site/ .env # mcman: Exclude exported mrpacks *.mrpack + +# mcman: Exclude local dotenv files +**/.env \ No newline at end of file diff --git a/examples/network/README.md b/examples/network/README.md new file mode 100644 index 0000000..aa4d953 --- /dev/null +++ b/examples/network/README.md @@ -0,0 +1,10 @@ +# CoolNetwork + +[![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) + + + +## Servers + + + diff --git a/examples/network/network.toml b/examples/network/network.toml new file mode 100644 index 0000000..936a4ee --- /dev/null +++ b/examples/network/network.toml @@ -0,0 +1,9 @@ +name = "CoolNetwork" +proxy = "proxy" +port = 25565 + +[servers.lobby] +port = 25566 +groups = [] + +[variables] diff --git a/examples/network/servers/lobby/README.md b/examples/network/servers/lobby/README.md new file mode 100644 index 0000000..9eab02f --- /dev/null +++ b/examples/network/servers/lobby/README.md @@ -0,0 +1,13 @@ +# lobby + +[![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) + + + + + + +## Plugins + + + diff --git a/examples/network/servers/lobby/config/server.properties b/examples/network/servers/lobby/config/server.properties new file mode 100644 index 0000000..fb89526 --- /dev/null +++ b/examples/network/servers/lobby/config/server.properties @@ -0,0 +1,2 @@ +server-port=${PORT:25565} +motd=${SERVER_NAME:A Minecraft Server} diff --git a/examples/network/servers/lobby/server.toml b/examples/network/servers/lobby/server.toml new file mode 100644 index 0000000..2ad265a --- /dev/null +++ b/examples/network/servers/lobby/server.toml @@ -0,0 +1,17 @@ +name = "lobby" +mc_version = "1.20.2" + +[jar] +type = "paper" +build = "latest" + +[variables] +PORT = "25565" + +[launcher] +nogui = true +preset_flags = "Aikars" +eula_args = true + +[options] +upload_to_mclogs = false diff --git a/res/default_readme b/res/default_readme index f151c33..91f7608 100644 --- a/res/default_readme +++ b/res/default_readme @@ -10,4 +10,4 @@ ## {ADDON_HEADER} - \ No newline at end of file + diff --git a/res/default_readme_network b/res/default_readme_network new file mode 100644 index 0000000..ca07b67 --- /dev/null +++ b/res/default_readme_network @@ -0,0 +1,10 @@ +# {NETWORK_NAME} + +[![mcman badge](https://img.shields.io/badge/uses-mcman-purple?logo=github)](https://github.com/ParadigmMC/mcman) + + + +## Servers + + + diff --git a/src/app/feedback.rs b/src/app/feedback.rs index f124445..ce7eb62 100644 --- a/src/app/feedback.rs +++ b/src/app/feedback.rs @@ -53,6 +53,12 @@ impl App { ))) } + pub fn ci(&self, cmd: &str) { + if std::env::var("CI").ok() == Some("true".to_owned()) { + self.multi_progress.suspend(|| println!("{cmd}")) + } + } + pub fn prompt_string(&self, prompt: &str) -> Result { Ok(self.multi_progress.suspend(|| { Input::with_theme(&ColorfulTheme::default()) diff --git a/src/app/mod.rs b/src/app/mod.rs index fc37f1b..a22acd0 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -141,7 +141,9 @@ impl App { } }, + // why not "TECHNOBLADE" => Some("Technoblade never dies".to_owned()), + "denizs_gf" => Some("ily may".to_owned()), k => if let Some(v) = std::env::var(k).ok() { Some(v) diff --git a/src/commands/export/mrpack.rs b/src/commands/export/mrpack.rs index 0227372..955b536 100644 --- a/src/commands/export/mrpack.rs +++ b/src/commands/export/mrpack.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use crate::app::App; +use crate::{app::App, interop::mrpack::MRPackWriter}; #[derive(clap::Args)] pub struct Args { @@ -33,7 +33,7 @@ pub async fn run(mut app: App, args: Args) -> Result<()> { let output_file = std::fs::File::create(output_filename).context("Creating mrpack output file")?; - app.mrpack().export_all(output_file).await?; + app.mrpack().export_all(MRPackWriter::from_writer(output_file)).await?; Ok(()) } diff --git a/src/commands/import/mrpack.rs b/src/commands/import/mrpack.rs index 87f7f40..59f7548 100644 --- a/src/commands/import/mrpack.rs +++ b/src/commands/import/mrpack.rs @@ -4,7 +4,7 @@ use anyhow::Result; use indicatif::ProgressBar; use tempfile::Builder; -use crate::app::App; +use crate::{app::App, interop::mrpack::MRPackReader}; #[derive(clap::Args)] pub struct Args { @@ -25,7 +25,7 @@ pub async fn run(mut app: App, args: Args) -> Result<()> { std::fs::File::open(path)? }; - app.mrpack().import_all(f, None).await?; + app.mrpack().import_all(MRPackReader::from_reader(f)?, None).await?; app.save_changes()?; app.refresh_markdown().await?; diff --git a/src/commands/init/init.rs b/src/commands/init/init.rs deleted file mode 100644 index e90b3a0..0000000 --- a/src/commands/init/init.rs +++ /dev/null @@ -1,51 +0,0 @@ -use anyhow::{Result, Context}; -use dialoguer::{Input, theme::ColorfulTheme, Select}; - -use crate::{app::App, model::{ServerLauncher, ServerType}}; - -pub async fn init_normal(app: &mut App) -> Result<()> { - let serv_type = Select::with_theme(&ColorfulTheme::default()) - .with_prompt("Type of server?") - .default(0) - .items(&[ - "Normal Server (vanilla, spigot, paper etc.)", - "Modded Server (forge, fabric, quilt etc.)", - "Proxy Server (velocity, bungeecord, waterfall etc.)", - ]) - .interact()?; - - let is_proxy = serv_type == 2; - - app.server.mc_version = if is_proxy { - "latest".to_owned() - } else { - let latest_ver = app.vanilla().fetch_latest_mcver() - .await - .context("Fetching latest version")?; - - Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Server version?") - .default(latest_ver) - .interact_text()? - }; - - app.server.launcher = if is_proxy { - ServerLauncher { - proxy_flags: true, - aikars_flags: false, - nogui: false, - ..Default::default() - } - } else { - ServerLauncher::default() - }; - - app.server.jar = match serv_type { - 0 => ServerType::select_jar_interactive(), - 1 => ServerType::select_modded_jar_interactive(), - 2 => ServerType::select_proxy_jar_interactive(), - _ => unreachable!(), - }?; - - Ok(()) -} diff --git a/src/commands/init/mod.rs b/src/commands/init/mod.rs index 0f47e8b..3a64e42 100644 --- a/src/commands/init/mod.rs +++ b/src/commands/init/mod.rs @@ -1,27 +1,22 @@ use console::style; -use dialoguer::Confirm; use dialoguer::{theme::ColorfulTheme, Input}; -use indicatif::{MultiProgress, ProgressBar}; +use indicatif::ProgressBar; +use rpackwiz::model::Pack; use std::ffi::OsStr; use std::fs::File; use std::io::Write; use std::path::Path; use tempfile::Builder; -use crate::commands::init::init::init_normal; -use crate::commands::init::network::init_network; -use crate::commands::markdown; -use crate::app::{BaseApp, App}; -use crate::model::Network; -use crate::util::env::{get_docker_version, write_dockerfile, write_dockerignore, write_gitignore}; +use crate::app::BaseApp; +use crate::interop::mrpack::MRPackReader; +use crate::interop::packwiz::FileProvider; +use crate::model::{Network, ServerType, SoftwareType, PresetFlags, ServerEntry}; +use crate::util::SelectItem; +use crate::util::env::{get_docker_version, write_dockerfile, write_dockerignore, write_gitignore, write_gitattributes, write_git}; use crate::model::Server; use anyhow::{bail, Context, Result}; -pub mod init; -pub mod network; -pub mod packwiz; -pub mod mrpack; - #[derive(clap::Args)] pub struct Args { /// The name of the server @@ -38,27 +33,16 @@ pub struct Args { network: bool, } -#[allow(dead_code)] +#[derive(Debug, Clone)] pub enum InitType { Normal, MRPack(String), - Packwiz(String), + Packwiz(FileProvider), Network, } #[allow(clippy::too_many_lines)] pub async fn run(base_app: BaseApp, args: Args) -> Result<()> { - println!(" > {}", style("Initializing new server...").cyan()); - - let res = std::fs::metadata("server.toml"); - if let Err(err) = res { - if err.kind() != std::io::ErrorKind::NotFound { - Err(err)?; - } - } else { - bail!("server.toml already exists"); - } - let current_dir = std::env::current_dir()?; let name = if let Some(name) = args.name { name.clone() @@ -70,44 +54,104 @@ pub async fn run(base_app: BaseApp, args: Args) -> Result<()> { .to_owned() }; - let theme = ColorfulTheme::default(); + let mut app = base_app.upgrade_with_default_server()?; - if args.network { - let name = Input::::with_theme(&theme) - .with_prompt("Network name?") - .default(name.clone()) - .with_initial_text(&name) - .interact_text()?; - - let mut app = App { - http_client: base_app.http_client, - network: Some(Network { - name, - ..Default::default() - }), - server: Server::default(), - multi_progress: MultiProgress::new(), - }; - - init_network(&mut app).await?; + let ty = if args.network { + InitType::Network } else { - let name = Input::::with_theme(&theme) - .with_prompt("Server name?") - .default(name.clone()) - .with_initial_text(&name) - .interact_text()?; - - let mut app = App { - http_client: base_app.http_client, - network: None, - server: Server { - name: name.clone(), - ..Default::default() - }, - multi_progress: MultiProgress::new(), - }; - if let Some(src) = args.mrpack { + InitType::MRPack(src.clone()) + } else if let Some(src) = args.packwiz { + InitType::Packwiz(app.packwiz().get_file_provider(&src)?) + } else { + InitType::Normal + } + }; + + // toml checks and init + match ty { + InitType::Network => { + if let Some(_nw) = Network::load()? { + bail!("network.toml already exists"); + } + + app.network = Some(Network::default()); + app.network.as_mut().unwrap().name = name.clone(); + } + _ => { + if let Some(serv) = Server::load().ok() { + bail!("server.toml already exists: server with name '{}'", serv.name); + } + + if let Some(nw) = Network::load()? { + app.info(format!("Creating a server inside the '{}' network", nw.name))?; + app.network = Some(nw); + } + + app.server.name = name.clone(); + } + } + + // Name + match &ty { + InitType::Normal | InitType::MRPack(_) => { + app.server.name = app.prompt_string_filled("Server name?", &app.server.name)?; + } + InitType::Packwiz(source) => { + let pack = source.parse_toml::("pack.toml") + .await + .context("Reading pack.toml - does it exist?")?; + + app.server.name = app.prompt_string_filled("Server name?", &pack.name)?; + } + InitType::Network => { + app.network.as_mut().unwrap().name = app.prompt_string_filled("Network name?", &app.network.as_ref().unwrap().name)?; + } + } + + match &ty { + InitType::Normal => { + let serv_type = app.select("Type of server?", &[ + SelectItem(SoftwareType::Normal, "Normal Server (vanilla, spigot, paper etc.)".to_owned()), + SelectItem(SoftwareType::Modded, "Modded Server (forge, fabric, quilt etc.)".to_owned()), + SelectItem(SoftwareType::Proxy, "Proxy Server (velocity, bungeecord, waterfall etc.)".to_owned()), + ])?; + + app.server.mc_version = if serv_type == SoftwareType::Proxy { + "latest".to_owned() + } else { + let latest_ver = app.vanilla().fetch_latest_mcver() + .await + .context("Fetching latest version")?; + + app.prompt_string_default("Server version?", &latest_ver)? + }; + + app.server.launcher.nogui = serv_type != SoftwareType::Proxy; + app.server.launcher.preset_flags = match serv_type { + SoftwareType::Proxy => PresetFlags::Proxy, + _ => PresetFlags::Aikars, + }; + + app.server.jar = match serv_type { + SoftwareType::Normal => ServerType::select_jar_interactive(), + SoftwareType::Modded => ServerType::select_modded_jar_interactive(), + SoftwareType::Proxy => ServerType::select_proxy_jar_interactive(), + _ => unreachable!(), + }?; + } + + InitType::Network => { + let nw = app.network.as_mut().unwrap(); + + let port = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Which port should the network be on?") + .default(25565 as u16) + .interact_text()?; + + nw.port = port; + } + InitType::MRPack(src) => { let tmp_dir = Builder::new().prefix("mcman-mrpack-import").tempdir()?; let f = if Path::new(&src).exists() { @@ -118,77 +162,131 @@ pub async fn run(base_app: BaseApp, args: Args) -> Result<()> { let path = tmp_dir.path().join(&resolved.filename); std::fs::File::open(path)? }; - app.mrpack().import_all(f, Some(name)).await?; - } else if let Some(src) = args.packwiz { - app.packwiz().import_all(&src).await?; - } else { - init_normal(&mut app).await?; + + app.mrpack().import_all(MRPackReader::from_reader(f)?, None).await?; + } + InitType::Packwiz(src) => { + app.packwiz().import_from_source(src.clone()).await?; } } - Ok(()) -} + match ty { + InitType::Network => app.network.as_ref().unwrap().save().context("Saving network.toml")?, + _ => app.server.save().context("Saving server.toml")?, + } -pub async fn init_final( - app: &App, - server: &mut Server, - is_proxy: bool, -) -> Result<()> { - app.server.save()?; + match ty { + InitType::Network => {} + _ => if let Some(ref mut nw) = app.network { + if nw.servers.contains_key(&app.server.name) { + app.warn("Server with that name already exists in network.toml, please add entry manually")?; + } else { + nw.servers.insert(app.server.name.clone(), ServerEntry { + port: nw.next_port(), + ..Default::default() + }); + nw.save()?; + drop(nw); + app.info("Added server entry to network.toml")?; + } + } + } - initialize_environment(is_proxy).context("Initializing environment")?; + //env + match ty { + InitType::Network => { + std::fs::create_dir_all("./servers")?; + } + _ => { + std::fs::create_dir_all("./config")?; + + if app.server.jar.get_software_type() != SoftwareType::Proxy { + let mut f = File::create("./config/server.properties")?; + f.write_all(include_bytes!("../../../res/server.properties"))?; + } + } + } let write_readme = if Path::new("./README.md").exists() { - Confirm::with_theme(&ColorfulTheme::default()) - .default(true) - .with_prompt("Overwrite README.md?") - .interact()? - } else { - true - }; + app.confirm("Overwrite README.md?")? + } else { true }; if write_readme { - markdown::initialize_readme(server).context("Initializing readme")?; + match ty { + InitType::Network => app.markdown().init_network()?, + _ => app.markdown().init_server()?, + } - server.markdown.files = vec!["README.md".to_owned()]; - server.save()?; + // TODO + //app.server.markdown.files = vec!["README.md".to_owned()]; + //app.server.save()?; - app.markdown().update_files().await?; + match ty { + InitType::MRPack(_) | InitType::Packwiz(_) => { + if app.confirm("Render markdown now?")? { + app.markdown().update_files().await?; + } + } + _ => {} + } } - println!( - " > {} {}", - style(&server.name).bold(), - style("has been initialized!").green() - ); + initialize_environment()?; - println!( - " > {} {}", - style("Build using").cyan(), - style("mcman build").bold() - ); + match ty { + InitType::Network => { + println!( + " > {} {} {}", + style("Network").green(), + style(&app.network.unwrap().name).bold(), + style("has been created!").green() + ); + + println!( + " > {}", + style("Initialize servers in this network using").cyan() + ); + println!( + " {}\n {}\n {}", + style("cd servers").bold(), + style("mkdir myserver").bold(), + style("mcman init").bold(), + ); + } + _ => { + println!( + " > {} {}", + style(&app.server.name).bold(), + style("has been initialized!").green() + ); + + println!( + " > {} {}", + style("Build using").cyan(), + style("mcman build").bold() + ); + } + } Ok(()) } -pub fn initialize_environment(is_proxy: bool) -> Result<()> { - std::fs::create_dir_all("./config")?; - +pub fn initialize_environment() -> Result<()> { let theme = ColorfulTheme::default(); - if write_gitignore().is_err() { + if write_git().is_err() { println!( "{} {}{}{}", theme.prompt_prefix, style("Didn't find a git repo, use '").dim(), style("mcman env gitignore").bold(), - style("' to generate .gitignore").dim(), + style("' to generate .gitignore/attributes").dim(), ); } else { println!( "{} {}", theme.success_prefix, - style("Touched up .gitignore").dim(), + style("Touched up .gitignore/.gitattributes").dim(), ); } @@ -204,16 +302,11 @@ pub fn initialize_environment(is_proxy: bool) -> Result<()> { println!( "{} {}{}{}", theme.prompt_prefix, - style("Docker wasn't found, you can use '").dim(), + style("Docker wasn't found, use '").dim(), style("mcman env docker").bold(), style("' to generate docker files").dim(), ); } - if !is_proxy { - let mut f = File::create("./config/server.properties")?; - f.write_all(include_bytes!("../../../res/server.properties"))?; - } - Ok(()) } diff --git a/src/commands/init/mrpack.rs b/src/commands/init/mrpack.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/commands/init/network.rs b/src/commands/init/network.rs deleted file mode 100644 index fca3544..0000000 --- a/src/commands/init/network.rs +++ /dev/null @@ -1,34 +0,0 @@ -use anyhow::Result; -use console::style; -use dialoguer::{theme::ColorfulTheme, Input}; - -use crate::app::App; - -pub async fn init_network(app: &mut App) -> Result<()> { - let nw = app.network.as_mut().unwrap(); - - let port = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Which port should the network be on?") - .default(25565 as u16) - .interact_text()?; - - nw.port = port; - - nw.save()?; - - println!( - " > {} {} {}", - style("Network").green(), - style(&nw.name).bold(), - style("has been initialized!").green() - ); - - println!( - " > {} {} {}", - style("Initialize servers in this network using").cyan(), - style("mcman init").bold(), - style("inside sub-folders").cyan(), - ); - - Ok(()) -} diff --git a/src/commands/init/packwiz.rs b/src/commands/init/packwiz.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/core/addons.rs b/src/core/addons.rs index 0037288..6f722e4 100644 --- a/src/core/addons.rs +++ b/src/core/addons.rs @@ -25,6 +25,8 @@ impl<'a> BuildContext<'a> { if server_list.len() < 200 { "" } else { " may god help you" }, ))?; + self.app.ci(&format!("::group::{addon_type}s")); + let mut files_list = HashSet::new(); let pb = ProgressBar::new(server_list.len() as u64) @@ -65,6 +67,8 @@ impl<'a> BuildContext<'a> { ))?; } + self.app.ci("::endgroup::"); + Ok(()) } } diff --git a/src/core/bootstrap.rs b/src/core/bootstrap.rs index 92bb0a4..8edc6c3 100644 --- a/src/core/bootstrap.rs +++ b/src/core/bootstrap.rs @@ -14,6 +14,8 @@ impl<'a> BuildContext<'a> { pub async fn bootstrap_files(&mut self) -> Result<()> { self.app.print_job("Bootstrapping...")?; + self.app.ci("::group::Bootstrapping"); + let pb = self.app.multi_progress.add(ProgressBar::new_spinner() .with_style(ProgressStyle::with_template("{spinner:.blue} {prefix} {msg}")?) .with_prefix("Bootstrapping")); @@ -62,6 +64,8 @@ impl<'a> BuildContext<'a> { pb.finish_and_clear(); self.app.success("Bootstrapping complete")?; + self.app.ci("::endgroup::"); + Ok(()) } diff --git a/src/core/mod.rs b/src/core/mod.rs index ec43a2a..95d4f1b 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -49,7 +49,9 @@ impl<'a> BuildContext<'a> { // actual stages contained here + self.app.ci("::group::Server Jar"); let server_jar = self.download_server_jar().await?; + self.app.ci("::endgroup::"); if !self.app.server.plugins.is_empty() { self.download_addons(AddonType::Plugin).await?; diff --git a/src/core/worlds.rs b/src/core/worlds.rs index 056d83c..e397782 100644 --- a/src/core/worlds.rs +++ b/src/core/worlds.rs @@ -13,6 +13,8 @@ impl<'a> BuildContext<'a> { .with_style(ProgressStyle::with_template("{prefix:.bold} {msg} [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})")?) .with_message("World:")); + self.app.ci("::group::Worlds"); + for (name, world) in self.app.server.worlds.iter().progress_with(progress_bar.clone()) { progress_bar.set_message(name.clone()); @@ -23,6 +25,8 @@ impl<'a> BuildContext<'a> { ).await.context(format!("Processing world: {name}"))?; } + self.app.ci("::endgroup::"); + Ok(()) } diff --git a/src/hot_reload/mod.rs b/src/hot_reload/mod.rs index cfe9100..82dec68 100644 --- a/src/hot_reload/mod.rs +++ b/src/hot_reload/mod.rs @@ -49,8 +49,8 @@ async fn try_wait_child(opt: &mut Option) -> Result> { } } +#[derive(Debug, PartialEq, Clone)] pub enum TestResult { - Ongoing, Success, Failed, Crashed, @@ -93,14 +93,14 @@ impl<'a> DevSession<'a> { async fn handle_commands(mut self, mut rx: mpsc::Receiver, mut tx: mpsc::Sender) -> Result<()> { let mp = self.builder.app.multi_progress.clone(); - let mut child: Option = None; - //let mut child_stdout = None; + self.builder.app.ci("::group::Starting server process"); + let mut child: Option = None; let mut stdout_lines: Option>> = None; let mut is_stopping = false; - let mut test_passed = false; - let mut test_result = TestResult::Ongoing; + let mut is_session_ending = false; + let mut test_result = TestResult::Failed; let mut exit_status = None; let mut stdin_lines = tokio::io::BufReader::new(tokio::io::stdin()).lines(); @@ -129,6 +129,7 @@ impl<'a> DevSession<'a> { Command::SendCommand(command) => { if let Some(ref mut child) = &mut child { if let Some(ref mut stdin) = &mut child.stdin { + eprintln!("checkpoint 2"); let _ = stdin.write_all(command.as_bytes()).await; } } @@ -204,10 +205,8 @@ impl<'a> DevSession<'a> { if self.test_mode && !is_stopping - && !test_passed { - + && test_result == TestResult::Failed { if s.contains("]: Done") && s.ends_with("For help, type \"help\"") { - test_passed = true; test_result = TestResult::Success; self.builder.app.success("Test passed!")?; @@ -234,16 +233,16 @@ impl<'a> DevSession<'a> { Ok(Some(line)) = stdin_lines.next_line() => { let mut cmd = line.trim(); - self.builder.app.info(&format!("Sending command: {cmd}"))?; + //self.builder.app.info(&format!("Sending command: {cmd}"))?; if let Some(ref mut child) = &mut child { if let Some(ref mut stdin) = &mut child.stdin { + eprintln!("checkpoint 1"); let _ = stdin.write_all(format!("{cmd}\n").as_bytes()).await; } } }, - status = try_wait_child(&mut child) => { - exit_status = status.unwrap_or(None); - + Ok(Some(status)) = try_wait_child(&mut child) => { + exit_status = Some(status); self.builder.app.info("Server process exited")?; is_stopping = false; @@ -255,9 +254,15 @@ impl<'a> DevSession<'a> { } }, _ = tokio::signal::ctrl_c() => { - if !is_stopping { - self.builder.app.info("Stopping development session...")?; + if is_session_ending { + self.builder.app.info("Force-stopping development session...")?; break 'l; + } else if !is_stopping { + self.builder.app.info("Stopping development session...")?; + + tx.send(Command::SendCommand("stop\nend\n".to_owned())).await?; + tx.send(Command::WaitUntilExit).await?; + tx.send(Command::EndSession).await?; } } } @@ -270,91 +275,103 @@ impl<'a> DevSession<'a> { child.kill().await?; } - if self.test_mode && !test_passed { - mp.suspend(|| { - println!( - "{} Test failed!", - ColorfulTheme::default().error_prefix - ); + self.builder.app.ci("::endgroup::"); - if let Some(status) = &exit_status { - if let Some(code) = status.code() { + if self.test_mode { + match test_result { + TestResult::Success => { + self.builder.app.success("Test passed")?; + std::process::exit(0); + } + TestResult::Crashed | TestResult::Failed => { + mp.suspend(|| { println!( - " - Process exited with code {}", - if code == 0 { - style(code).green() + "{} Test failed!", + ColorfulTheme::default().error_prefix + ); + + if let Some(status) = &exit_status { + if let Some(code) = status.code() { + println!( + " - Process exited with code {}", + if code == 0 { + style(code).green() + } else { + style(code).red().bold() + } + ); } else { - style(code).red().bold() + if !status.success() { + println!( + " - Process didn't exit successfully" + ); + } } - ); - } else { - if !status.success() { - println!( - " - Process didn't exit successfully" - ); } - } - } - - match test_result { - TestResult::Crashed => { - println!( - " - Server crashed" - ); - } - _ => {} - } - }); - - if self.builder.app.server.options.upload_to_mclogs { - let pb = mp.add(ProgressBar::new_spinner() - .with_message("Uploading to mclo.gs")); - - pb.enable_steady_tick(Duration::from_millis(250)); - - let log_path = match test_result { - TestResult::Crashed => { - let folder = self.builder.output_dir.join("crash-reports"); - if !folder.exists() { - bail!("crash-reports folder doesn't exist, cant upload to mclo.gs"); + + match test_result { + TestResult::Crashed => { + println!( + " - Server crashed" + ); + } + _ => {} } + }); - // get latest crash report - let (report_path, _) = folder.read_dir()? - .into_iter() - .filter_map(|f| f.ok()) - .filter_map(|f| Some((f.path(), f.metadata().ok()?.modified().ok()?))) - .max_by_key(|(_, t)| t.clone()) - .ok_or(anyhow!("can't find crash report"))?; - - report_path - } - _ => { - self.builder.output_dir.join("logs").join("latest.log") + if self.builder.app.server.options.upload_to_mclogs { + let pb = mp.add(ProgressBar::new_spinner() + .with_message("Uploading to mclo.gs")); + + pb.enable_steady_tick(Duration::from_millis(250)); + + let log_path = match test_result { + TestResult::Crashed => { + let folder = self.builder.output_dir.join("crash-reports"); + if !folder.exists() { + bail!("crash-reports folder doesn't exist, cant upload to mclo.gs"); + } + + // get latest crash report + let (report_path, _) = folder.read_dir()? + .into_iter() + .filter_map(|f| f.ok()) + .filter_map(|f| Some((f.path(), f.metadata().ok()?.modified().ok()?))) + .max_by_key(|(_, t)| t.clone()) + .ok_or(anyhow!("can't find crash report"))?; + + report_path + } + _ => { + self.builder.output_dir.join("logs").join("latest.log") + } + }; + + if log_path.exists() { + let content = std::fs::read_to_string(&log_path) + .context("Reading log file")?; + + let log = self.builder.app.mclogs().paste_log(&content).await?; + drop(content); + + pb.finish_and_clear(); + self.builder.app.log(" - Log uploaded to mclo.gs")?; + mp.suspend(|| { + println!(); + println!(" -- [ {} ] --", log.url); + println!(); + }); + } else { + pb.finish_and_clear(); + mp.suspend(|| println!( + "{} '{}' does not exist! Can't upload log.", + ColorfulTheme::default().error_prefix, + style(log_path.to_string_lossy()).dim() + )); + } } - }; - - if log_path.exists() { - let content = std::fs::read_to_string(&log_path) - .context("Reading log file")?; - - let log = self.builder.app.mclogs().paste_log(&content).await?; - drop(content); - - pb.finish_and_clear(); - self.builder.app.log(" - Log uploaded to mclo.gs")?; - mp.suspend(|| { - println!(); - println!(" -- [ {} ] --", log.url); - println!(); - }); - } else { - pb.finish_and_clear(); - mp.suspend(|| println!( - "{} '{}' does not exist! Can't upload log.", - ColorfulTheme::default().error_prefix, - style(log_path.to_string_lossy()).dim() - )); + + std::process::exit(1); } } } diff --git a/src/interop/java.rs b/src/interop/java.rs new file mode 100644 index 0000000..2672a26 --- /dev/null +++ b/src/interop/java.rs @@ -0,0 +1,12 @@ +use std::path::PathBuf; + +use crate::app::App; + +pub struct JavaVersion(pub PathBuf, pub String); + +pub struct JavaAPI<'a>(pub &'a App); + +impl<'a> JavaAPI<'a> { + +} + diff --git a/src/interop/markdown.rs b/src/interop/markdown.rs index ca2db1e..76da449 100644 --- a/src/interop/markdown.rs +++ b/src/interop/markdown.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::{time::Duration, fs::File, io::Write}; use anyhow::Result; use indexmap::IndexMap; @@ -16,6 +16,36 @@ pub struct MarkdownTemplate { pub struct MarkdownAPI<'a>(pub &'a App); impl<'a> MarkdownAPI<'a> { + pub fn init_server(&self) -> Result<()> { + let mut f = File::create(self.0.server.path.join("README.md"))?; + let readme_content = include_str!("../../res/default_readme"); + let readme_content = readme_content + .replace("{SERVER_NAME}", &self.0.server.name) + .replace( + "{ADDON_HEADER}", + if self.0.server.jar.is_modded() { + "Mods" + } else { + "Plugins" + }, + ); + + f.write_all(readme_content.as_bytes())?; + + Ok(()) + } + + pub fn init_network(&self) -> Result<()> { + let mut f = File::create(self.0.network.as_ref().unwrap().path.join("README.md"))?; + let readme_content = include_str!("../../res/default_readme_network"); + let readme_content = readme_content + .replace("{NETWORK_NAME}", &self.0.network.as_ref().unwrap().name); + + f.write_all(readme_content.as_bytes())?; + + Ok(()) + } + pub async fn update_files(&self) -> Result<()> { let templates = self.get_templates().await?; @@ -23,13 +53,23 @@ impl<'a> MarkdownAPI<'a> { .with_style(ProgressStyle::with_template("{prefix:.blue.bold} {msg} [{wide_bar:.cyan/blue}] {pos}/{len}")?) .with_prefix("Writing to"); - for file in self.0.server.markdown.files.iter().progress_with(pb.clone()) { - pb.set_message(file.clone()); + let mut files = self.0.server.markdown.files + .iter() + .map(|f| (false, self.0.server.path.join(f))) + .collect::>(); + if let Some(nw) = &self.0.network { + files.extend(nw.markdown.files + .iter() + .map(|f| (true, nw.path.join(f)))); + } + + for (_is_nw, path) in files.iter().progress_with(pb.clone()) { + let filename = path.file_name().unwrap().to_string_lossy(); - let path = self.0.server.path.join(file); + pb.set_message(filename.to_string()); if !path.exists() { - self.0.warn(format!("{file} does not exist! Skipping"))?; + self.0.warn(format!("{filename} does not exist! Skipping"))?; continue; } diff --git a/src/interop/mod.rs b/src/interop/mod.rs index b94d04b..7f83135 100644 --- a/src/interop/mod.rs +++ b/src/interop/mod.rs @@ -1,3 +1,4 @@ pub mod mrpack; pub mod packwiz; pub mod markdown; +pub mod java; diff --git a/src/interop/mrpack.rs b/src/interop/mrpack.rs index e330536..3bb9aed 100644 --- a/src/interop/mrpack.rs +++ b/src/interop/mrpack.rs @@ -15,9 +15,9 @@ pub struct MRPackInterop<'a>(pub &'a mut App); impl<'a> MRPackInterop<'a> { pub async fn import_all( &mut self, - reader: R, + mut mrpack: MRPackReader, name: Option - ) -> Result<()> { + ) -> Result { let progress_bar = self.0.multi_progress.add(ProgressBar::new_spinner() .with_finish(ProgressFinish::WithMessage("Imported".into()))); progress_bar.set_message(name.unwrap_or("mrpack".to_owned()).clone()); @@ -25,8 +25,6 @@ impl<'a> MRPackInterop<'a> { progress_bar.set_prefix("Reading zip file"); progress_bar.enable_steady_tick(Duration::from_millis(250)); - let mut mrpack = MRPackReader::from_reader(reader)?; - progress_bar.set_prefix("Reading index of"); let index = mrpack.read_index()?; @@ -75,20 +73,18 @@ impl<'a> MRPackInterop<'a> { self.0.success("mrpack imported!")?; - Ok(()) + Ok(index) } pub async fn export_all( &self, - writer: W, + mut mrpack: MRPackWriter, ) -> Result<()> { let progress_bar = self.0.multi_progress.add(ProgressBar::new_spinner() .with_finish(ProgressFinish::WithMessage("Exported".into()))); progress_bar.set_message("Exporting mrpack..."); progress_bar.enable_steady_tick(Duration::from_millis(250)); - let mut mrpack = MRPackWriter::from_writer(writer); - let mut files = vec![]; let pb = self.0.multi_progress.insert_after(&progress_bar, ProgressBar::new_spinner() diff --git a/src/interop/packwiz.rs b/src/interop/packwiz.rs index c63828e..8b3b2da 100644 --- a/src/interop/packwiz.rs +++ b/src/interop/packwiz.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, time::Duration, collections::HashMap}; +use std::{path::PathBuf, time::Duration, collections::HashMap, str::FromStr}; use anyhow::{anyhow, Result}; use indicatif::{ProgressBar, ProgressFinish, ProgressStyle, ProgressIterator}; @@ -7,6 +7,7 @@ use serde::de::DeserializeOwned; use crate::{app::{App, Resolvable, ResolvedFile, CacheStrategy}, model::Downloadable}; +#[derive(Debug, Clone)] pub enum FileProvider { LocalFolder(PathBuf), RemoteURL(reqwest::Client, reqwest::Url), @@ -37,15 +38,19 @@ impl FileProvider { pub struct PackwizInterop<'a>(pub &'a mut App); impl<'a> PackwizInterop<'a> { + pub fn get_file_provider(&self, s: &str) -> Result { + Ok(if s.starts_with("http") { + FileProvider::RemoteURL(self.0.http_client.clone(), s.try_into()?) + } else { + FileProvider::LocalFolder(s.into()) + }) + } + pub async fn import_all( &mut self, from: &str, ) -> Result<()> { - self.import_from_source(if from.starts_with("http") { - FileProvider::RemoteURL(self.0.http_client.clone(), from.try_into()?) - } else { - FileProvider::LocalFolder(from.into()) - }).await + self.import_from_source(self.get_file_provider(from)?).await } pub async fn import_from_source( diff --git a/src/model/network/mod.rs b/src/model/network/mod.rs index 43d35d2..eb6ac2c 100644 --- a/src/model/network/mod.rs +++ b/src/model/network/mod.rs @@ -9,7 +9,7 @@ use std::{ use anyhow::Result; use serde::{Deserialize, Serialize}; -use super::{ClientSideMod, Downloadable}; +use super::{ClientSideMod, Downloadable, MarkdownOptions}; #[derive(Debug, Deserialize, Serialize)] #[serde(default)] @@ -29,6 +29,9 @@ pub struct Network { pub mods: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] pub clientsidemods: Vec, + + #[serde(skip_serializing_if = "MarkdownOptions::is_empty")] + pub markdown: MarkdownOptions, } impl Network { @@ -54,23 +57,38 @@ impl Network { pub fn load_from(path: &PathBuf) -> Result { let data = read_to_string(path)?; let mut nw: Self = toml::from_str(&data)?; - nw.path = path.clone(); + nw.path = path.parent().unwrap().to_path_buf(); Ok(nw) } pub fn save(&self) -> Result<()> { let cfg_str = toml::to_string_pretty(&self)?; - let mut f = File::create(&self.path)?; + let mut f = File::create(self.path.join("network.toml"))?; f.write_all(cfg_str.as_bytes())?; Ok(()) } + + pub fn next_port(&self) -> u16 { + let mut port = 25565; + + let mut taken = vec![self.port]; + for (_, serv) in &self.servers { + taken.push(serv.port); + } + + while taken.contains(&port) { + port += 1; + } + + port + } } impl Default for Network { fn default() -> Self { Self { - path: PathBuf::from("./network.toml"), + path: PathBuf::from("."), name: String::new(), proxy: "proxy".to_owned(), port: 25565, @@ -79,6 +97,7 @@ impl Default for Network { plugins: vec![], mods: vec![], clientsidemods: vec![], + markdown: MarkdownOptions::default(), } } } @@ -88,5 +107,6 @@ impl Default for Network { pub struct ServerEntry { pub port: u16, pub ip_address: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] pub groups: Vec, } diff --git a/src/model/serverlauncher.rs b/src/model/serverlauncher.rs index c73322f..cbd1c1a 100644 --- a/src/model/serverlauncher.rs +++ b/src/model/serverlauncher.rs @@ -2,13 +2,35 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum PresetFlags { + Aikars, + Proxy, + #[default] + None, +} + +impl PresetFlags { + pub fn get_flags(&self) -> Vec { + match self { + Self::Aikars => include_str!("../../res/aikars_flags"), + Self::Proxy => include_str!("../../res/proxy_flags"), + Self::None => "", + }.split(char::is_whitespace) + .map(ToOwned::to_owned) + .collect() + } +} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(default)] pub struct ServerLauncher { - pub aikars_flags: bool, - pub proxy_flags: bool, + #[serde(skip_serializing_if = "crate::util::is_default")] pub nogui: bool, #[serde(skip_serializing_if = "crate::util::is_default")] + pub preset_flags: PresetFlags, + #[serde(skip_serializing_if = "crate::util::is_default")] pub disable: bool, #[serde(skip_serializing_if = "crate::util::is_default")] pub jvm_args: String, @@ -20,6 +42,13 @@ pub struct ServerLauncher { pub memory: String, #[serde(skip_serializing_if = "crate::util::is_default")] pub properties: HashMap, + + #[serde(skip_serializing_if = "Vec::is_empty")] + pub prelaunch: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub postlaunch: Vec, + + pub java_version: Option, } #[derive(Debug, Clone)] @@ -32,16 +61,26 @@ 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()) { + path + } else { + String::from("java") + } + } + pub fn generate_script_linux(&self, _servername: &str, startup: &StartupMethod) -> String { format!( - "#!/bin/sh\n# generated by mcman\njava {} \"$@\"\n", + "#!/bin/sh\n# generated by mcman\n{} {} \"$@\"\n", + self.get_java(), self.get_arguments(startup, "linux").join(" ") ) } pub fn generate_script_win(&self, servername: &str, startup: &StartupMethod) -> String { format!( - "@echo off\r\n:: generated by mcman\r\ntitle {servername}\r\njava {} %*\r\n", + "@echo off\r\n:: generated by mcman\r\ntitle {servername}\r\n{} {} %*\r\n", + self.get_java(), self.get_arguments(startup, "windows").join(" ") ) } @@ -59,23 +98,9 @@ impl ServerLauncher { args.push(format!("-Xmx{m}")); } - if self.aikars_flags { - args.append( - &mut include_str!("../../res/aikars_flags") - .split(char::is_whitespace) - .map(ToOwned::to_owned) - .collect(), - ); - } - - if self.proxy_flags { - args.append( - &mut include_str!("../../res/proxy_flags") - .split(char::is_whitespace) - .map(ToOwned::to_owned) - .collect(), - ); - } + args.append( + &mut self.preset_flags.get_flags(), + ); if self.eula_args { args.push(String::from("-Dcom.mojang.eula.agree=true")); @@ -124,15 +149,17 @@ impl ServerLauncher { impl Default for ServerLauncher { fn default() -> Self { Self { - disable: false, + preset_flags: PresetFlags::None, + nogui: true, jvm_args: String::new(), game_args: String::new(), - aikars_flags: true, - proxy_flags: false, - nogui: true, + disable: false, eula_args: true, memory: String::new(), properties: HashMap::default(), + prelaunch: vec![], + postlaunch: vec![], + java_version: None, } } } diff --git a/src/model/servertoml/interactive.rs b/src/model/servertoml/interactive.rs deleted file mode 100644 index 4e16597..0000000 --- a/src/model/servertoml/interactive.rs +++ /dev/null @@ -1,52 +0,0 @@ -use anyhow::Result; -use dialoguer::{theme::ColorfulTheme, Select, Input}; - -use crate::{model::{Downloadable, World}, util::SelectItem}; - -use super::Server; - -impl Server { - pub fn add_datapack(&mut self, dl: Downloadable) -> Result { - let selected_world_name = if self.worlds.is_empty() { - "+".to_owned() - } else { - let mut items: Vec> = self - .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 !self.worlds.contains_key(&world_name) { - self.worlds.insert(world_name.clone(), World::default()); - } - - self - .worlds - .get_mut(&world_name) - .expect("world shouldve already been inserted") - .datapacks - .push(dl); - - Ok(world_name) - } -} diff --git a/src/model/servertoml/mod.rs b/src/model/servertoml/mod.rs index 9655a70..af25d9d 100644 --- a/src/model/servertoml/mod.rs +++ b/src/model/servertoml/mod.rs @@ -13,8 +13,6 @@ use crate::sources::modrinth; use super::{ClientSideMod, Downloadable, ServerLauncher, ServerType, World, SoftwareType}; -pub mod interactive; - #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)] #[serde(default)] pub struct MarkdownOptions { diff --git a/src/model/servertype/mod.rs b/src/model/servertype/mod.rs index 6952b71..dbabb18 100644 --- a/src/model/servertype/mod.rs +++ b/src/model/servertype/mod.rs @@ -13,11 +13,12 @@ pub mod interactive; pub mod meta; pub mod parse; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] pub enum SoftwareType { Normal, Modded, Proxy, + #[default] Unknown, } diff --git a/src/util/env.rs b/src/util/env.rs index 5c1a01f..8684147 100644 --- a/src/util/env.rs +++ b/src/util/env.rs @@ -41,6 +41,12 @@ pub fn get_git_remote() -> Result> { )) } +pub fn write_git() -> Result<()> { + write_gitignore()?; + write_gitattributes()?; + Ok(()) +} + pub fn write_gitignore() -> Result { let root = get_git_root()?.ok_or(anyhow!("cant get repo root"))?; @@ -56,7 +62,6 @@ pub fn write_gitignore() -> Result { for (ignore, comment) in [ ("**/server", "# mcman: Exclude mcman build outputs"), - (".env", "# mcman: Exclude env secrets"), ("*.mrpack", "# mcman: Exclude exported mrpacks"), ("**/.env", "# mcman: Exclude local dotenv files"), ] { @@ -75,6 +80,37 @@ pub fn write_gitignore() -> Result { Ok(gitignore_path) } +pub fn write_gitattributes() -> Result { + let root = get_git_root()?.ok_or(anyhow!("cant get repo root"))?; + + let gitattributes_path = Path::new(&root).join(".gitattibutes"); + + let contents = fs::read_to_string(&gitattributes_path).unwrap_or(String::new()); + + let has_r = contents.contains('\r'); + + let contents = contents.replace('\r', ""); + + let mut list = contents.split('\n').collect::>(); + + for (attr, comment) in [ + ("**/worlds/*.zip filter=lfs diff=lfs merge=lfs -text", "# mcman: use lfs for worlds"), + ] { + if !list.contains(&attr) { + if !comment.is_empty() { + list.push(comment); + } + list.push(attr); + } + } + + let contents = list.join(if has_r { "\r\n" } else { "\n" }); + + fs::write(&gitattributes_path, contents)?; + + Ok(gitattributes_path) +} + pub fn get_git_root() -> Result> { git_command(vec!["rev-parse", "--show-toplevel"]) }