diff --git a/Manual.md b/Manual.md index f8b321c..7513e2f 100644 --- a/Manual.md +++ b/Manual.md @@ -20,11 +20,12 @@ SQLite format maps are currently supported. ## General usage -`mapeditr [-h] ` +`mapeditr [-h] [-y] ` Arguments: -- `-h`: Show a help message and exit. +- `-h, --help`: Print help information and exit. +- `-y, --yes`: Skip the default confirmation prompt (for those who feel brave). - ``: Path to the Minetest world/map to edit; this can be either a world directory or a `map.sqlite` file within a world folder. This file will be modified, so *always* shut down the game/server before executing any command. @@ -75,8 +76,8 @@ Arguments: - `--p1, --p2`: Area containing mapblocks to delete. By default, only mapblocks fully within this area will be deleted. -- `--invert`: Delete all mapblocks fully *outside* the given area. Use with -caution; you could erase a large portion of your world! +- `--invert`: If present, delete all mapblocks fully *outside* the given area. +Use with caution; you could erase a large portion of your world! **Note:** Deleting mapblocks is *not* the same as filling them with air! Mapgen will be invoked where the blocks were deleted, and this sometimes causes @@ -91,11 +92,11 @@ contents) are also deleted. Arguments: -- `--node`: Only delete metadata of nodes with the given name. If not -specified, metadata will be deleted in all matching nodes. -- `--p1, --p2`: Area in which to delete metadata. If not specified, metadata -will be deleted everywhere. -- `--invert`: Only delete metadata *outside* the given area. +- `--node`: (Optional) Name of node to delete metadata from. If not specified, +metadata will be deleted from any node. +- `--p1, --p2`: (Optional) Area in which to delete metadata. If not specified, +metadata will be deleted everywhere. +- `--invert`: If present, delete metadata *outside* the given area. ### deleteobjects diff --git a/src/cmd_line.rs b/src/cmd_line.rs index b35f708..3d2d444 100644 --- a/src/cmd_line.rs +++ b/src/cmd_line.rs @@ -139,6 +139,12 @@ fn parse_cmd_line_args() -> anyhow::Result { For additional information, see the manual.") .version(crate_version!()) .author(crate_authors!()) + .arg(Arg::with_name("yes") + .long("yes") + .short("y") + .global(true) + .help("Skip the default confirmation prompt.") + ) // TODO: Move map arg to subcommands? .arg(Arg::with_name("map") .required(true) @@ -153,6 +159,7 @@ fn parse_cmd_line_args() -> anyhow::Result { let sub_matches = matches.subcommand_matches(&sub_name).unwrap(); Ok(InstArgs { + do_confirmation: !matches.is_present("yes"), command: sub_name, map_path: matches.value_of("map").unwrap().to_string(), input_map_path: sub_matches.value_of("input_map").map(str::to_string), @@ -214,7 +221,7 @@ fn print_editing_status(done: usize, total: usize, real_start: Instant, let num_bars = (progress * TOTAL_BARS as f32) as usize; let bars = "=".repeat(num_bars); - eprint!( + print!( "\r[{bars:>() .join(&format!( "\n{}", " ".repeat(prefix.len()) )); - eprintln!("{}{}", prefix, indented); + println!("{}{}", prefix, indented); +} + + +fn get_confirmation() -> bool { + print!("Proceed? (Y/n): "); + let mut result = String::new(); + std::io::stdin().read_line(&mut result).unwrap(); + result.trim().to_ascii_lowercase() == "y" } pub fn run_cmd_line() { use std::sync::mpsc; - use crate::instance::{InstState, InstEvent, spawn_compute_thread}; + use crate::instance::{InstState, ServerEvent, spawn_compute_thread}; let args = match parse_cmd_line_args() { Ok(a) => a, @@ -265,13 +280,24 @@ pub fn run_cmd_line() { let mut cur_state = InstState::Ignore; let mut need_newline = false; + let newline_if = |condition: &mut bool| { + if *condition { + println!(); + *condition = false; + } + }; + loop { /* Main command-line logging loop */ let now = Instant::now(); let mut forced_update = InstState::Ignore; - match status.event_rx.recv_timeout(TICK) { + match status.receiver().recv_timeout(TICK) { Ok(event) => match event { - InstEvent::NewState(new_state) => { + ServerEvent::Log(log_type, msg) => { + newline_if(&mut need_newline); + print_log(log_type, msg); + }, + ServerEvent::NewState(new_state) => { // Force progress updates at the beginning and end of // querying/editing stages. if (cur_state == InstState::Ignore) != @@ -290,13 +316,10 @@ pub fn run_cmd_line() { } cur_state = new_state; }, - InstEvent::Log(log_type, msg) => { - if need_newline { - eprintln!(); - need_newline = false; - } - print_log(log_type, msg); - } + ServerEvent::ConfirmRequest => { + newline_if(&mut need_newline); + status.confirm(get_confirmation()); + }, }, Err(err) => { // Compute thread has exited; break out of the loop. @@ -311,8 +334,8 @@ pub fn run_cmd_line() { if forced_update == InstState::Querying || (cur_state == InstState::Querying && timed_update_ready) { - eprint!("\rQuerying mapblocks... {} found.", - status.get().blocks_total); + print!("\rQuerying mapblocks... {} found.", + status.get_status().blocks_total); std::io::stdout().flush().unwrap(); last_update = now; need_newline = true; @@ -320,8 +343,7 @@ pub fn run_cmd_line() { else if forced_update == InstState::Editing || (cur_state == InstState::Editing && timed_update_ready) { - let s = status.get(); - // TODO: Update duration format? e.g. 1m 42s remaining + let s = status.get_status(); print_editing_status(s.blocks_done, s.blocks_total, querying_start, editing_start, s.show_progress); last_update = now; @@ -329,9 +351,8 @@ pub fn run_cmd_line() { } // Print a newline after the last querying/editing message. - if need_newline && cur_state == InstState::Ignore { - eprintln!(); - need_newline = false; + if cur_state == InstState::Ignore { + newline_if(&mut need_newline); } } diff --git a/src/commands/clone.rs b/src/commands/clone.rs index 9f4b21b..da80a67 100644 --- a/src/commands/clone.rs +++ b/src/commands/clone.rs @@ -1,4 +1,4 @@ -use super::{Command, BLOCK_CACHE_SIZE}; +use super::{Command, ArgResult, BLOCK_CACHE_SIZE}; use crate::{unwrap_or, opt_unwrap_or}; use crate::spatial::{Vec3, Area, MAP_LIMIT}; @@ -10,7 +10,7 @@ use crate::instance::{ArgType, InstBundle, InstArgs}; use crate::utils::{CacheMap, query_keys}; -fn verify_args(args: &InstArgs) -> anyhow::Result<()> { +fn verify_args(args: &InstArgs) -> ArgResult { let map_area = Area::new( Vec3::new(-MAP_LIMIT, -MAP_LIMIT, -MAP_LIMIT), Vec3::new(MAP_LIMIT, MAP_LIMIT, MAP_LIMIT) @@ -19,10 +19,10 @@ fn verify_args(args: &InstArgs) -> anyhow::Result<()> { if map_area.intersection(args.area.unwrap() + args.offset.unwrap()) .is_none() { - anyhow::bail!("Destination area is outside map bounds."); + return ArgResult::error("Destination area is outside map bounds."); } - Ok(()) + ArgResult::Ok } diff --git a/src/commands/delete_blocks.rs b/src/commands/delete_blocks.rs index ec91e85..68e33b6 100644 --- a/src/commands/delete_blocks.rs +++ b/src/commands/delete_blocks.rs @@ -25,7 +25,8 @@ pub fn get_command() -> Command { args: vec![ (ArgType::Area(true), "Area containing mapblocks to delete"), (ArgType::Invert, - "Delete all mapblocks fully *outside* the given area.") + "If present, delete all mapblocks fully *outside* the given \ + area.") ], help: "Delete all mapblocks inside or outside an area." } diff --git a/src/commands/delete_meta.rs b/src/commands/delete_meta.rs index d9c9d77..fd189b4 100644 --- a/src/commands/delete_meta.rs +++ b/src/commands/delete_meta.rs @@ -1,12 +1,22 @@ -use super::Command; +use super::{Command, ArgResult}; use crate::unwrap_or; use crate::spatial::Vec3; -use crate::instance::{ArgType, InstBundle}; +use crate::instance::{ArgType, InstArgs, InstBundle}; use crate::map_block::{MapBlock, NodeMetadataList, NodeMetadataListExt}; use crate::utils::{query_keys, to_bytes, to_slice, fmt_big_num}; +fn verify_args(args: &InstArgs) -> ArgResult { + if !args.area.is_some() && !args.node.is_some() { + return ArgResult::warning( + "No area or node specified. ALL metadata will be deleted!"); + } + + ArgResult::Ok +} + + fn delete_metadata(inst: &mut InstBundle) { let node = inst.args.node.as_ref().map(to_bytes); @@ -71,14 +81,13 @@ fn delete_metadata(inst: &mut InstBundle) { pub fn get_command() -> Command { Command { func: delete_metadata, - verify_args: None, + verify_args: Some(verify_args), args: vec![ (ArgType::Area(false), "Area in which to delete metadata"), - (ArgType::Invert, "Delete all metadata outside the given area."), - (ArgType::Node(false), - "Node to delete metadata from. If not specified, all metadata \ - will be deleted.") + (ArgType::Invert, + "If present, delete metadata *outside* the given area."), + (ArgType::Node(false), "Name of node to delete metadata from") ], - help: "Delete node metadata." + help: "Delete node metadata of certain nodes." } } diff --git a/src/commands/delete_objects.rs b/src/commands/delete_objects.rs index 5d4339e..32977ad 100644 --- a/src/commands/delete_objects.rs +++ b/src/commands/delete_objects.rs @@ -61,8 +61,7 @@ fn delete_objects(inst: &mut InstBundle) { // is specified. let search_item = inst.args.items.as_ref().and_then(|items| items.get(0)) .map(to_bytes); - let item_searcher = search_item.as_ref() - .map(|s| TwoWaySearcher::new(s)); + let item_searcher = search_item.as_ref().map(|s| TwoWaySearcher::new(s)); let keys = query_keys(&mut inst.db, &mut inst.status, to_slice(&search_obj), inst.args.area, inst.args.invert, true); @@ -71,8 +70,9 @@ fn delete_objects(inst: &mut InstBundle) { let mut count: u64 = 0; for key in keys { inst.status.inc_done(); - let data = unwrap_or!(inst.db.get_block(key), continue); - let mut block = unwrap_or!(MapBlock::deserialize(&data), continue); + let data = inst.db.get_block(key).unwrap(); + let mut block = unwrap_or!(MapBlock::deserialize(&data), + { inst.status.inc_failed(); continue; }); let mut modified = false; for i in (0..block.static_objects.len()).rev() { diff --git a/src/commands/mod.rs b/src/commands/mod.rs index f4f4144..768d2e5 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -19,9 +19,30 @@ mod vacuum; pub const BLOCK_CACHE_SIZE: usize = 1024; +pub enum ArgResult { + Ok, + Warning(String), + Error(String), +} + +impl ArgResult { + /// Create a new ArgResult::Warning from a &str. + #[inline] + pub fn warning(msg: &str) -> Self { + Self::Warning(msg.to_string()) + } + + /// Create a new ArgResult::Error from a &str. + #[inline] + pub fn error(msg: &str) -> Self { + Self::Error(msg.to_string()) + } +} + + pub struct Command { pub func: fn(&mut InstBundle), - pub verify_args: Option anyhow::Result<()>>, + pub verify_args: Option ArgResult>, pub help: &'static str, pub args: Vec<(ArgType, &'static str)> } diff --git a/src/commands/overlay.rs b/src/commands/overlay.rs index b247bfc..25c93f2 100644 --- a/src/commands/overlay.rs +++ b/src/commands/overlay.rs @@ -1,4 +1,4 @@ -use super::{Command, BLOCK_CACHE_SIZE}; +use super::{Command, ArgResult, BLOCK_CACHE_SIZE}; use crate::{unwrap_or, opt_unwrap_or}; use crate::spatial::{Vec3, Area, MAP_LIMIT}; @@ -10,11 +10,11 @@ use crate::block_utils::{merge_blocks, merge_metadata, clean_name_id_map}; use crate::utils::{query_keys, CacheMap}; -fn verify_args(args: &InstArgs) -> anyhow::Result<()> { +fn verify_args(args: &InstArgs) -> ArgResult { if args.invert && args.offset.filter(|&ofs| ofs != Vec3::new(0, 0, 0)).is_some() { - anyhow::bail!("Inverted selections cannot be offset."); + return ArgResult::error("Inverted selections cannot be offset."); } let offset = args.offset.unwrap_or(Vec3::new(0, 0, 0)); @@ -26,10 +26,10 @@ fn verify_args(args: &InstArgs) -> anyhow::Result<()> { if map_area.intersection(args.area.unwrap_or(map_area) + offset) .is_none() { - anyhow::bail!("Destination area is outside map bounds."); + return ArgResult::error("Destination area is outside map bounds."); } - Ok(()) + ArgResult::Ok } diff --git a/src/commands/replace_in_inv.rs b/src/commands/replace_in_inv.rs index 8720e3f..737211e 100644 --- a/src/commands/replace_in_inv.rs +++ b/src/commands/replace_in_inv.rs @@ -1,4 +1,4 @@ -use super::Command; +use super::{Command, ArgResult}; use crate::unwrap_or; use crate::spatial::Vec3; @@ -129,14 +129,15 @@ fn replace_in_inv(inst: &mut InstBundle) { } -fn verify_args(args: &InstArgs) -> anyhow::Result<()> { +fn verify_args(args: &InstArgs) -> ArgResult { if args.new_item.is_none() && !args.delete_item && !args.delete_meta { - anyhow::bail!( - "new_item is required unless --delete or --deletemeta is used.") + return ArgResult::error( + "new_item is required unless --delete or --deletemeta is used."); } else if args.new_item.is_some() && args.delete_item { - anyhow::bail!("Cannot delete items if new_item is specified."); + return ArgResult::error( + "Cannot delete items if new_item is specified."); } - Ok(()) + ArgResult::Ok } diff --git a/src/commands/replace_nodes.rs b/src/commands/replace_nodes.rs index 179ec70..2630707 100644 --- a/src/commands/replace_nodes.rs +++ b/src/commands/replace_nodes.rs @@ -1,4 +1,4 @@ -use super::Command; +use super::{Command, ArgResult}; use crate::unwrap_or; use crate::spatial::{Vec3, Area, InverseBlockIterator}; @@ -127,10 +127,12 @@ fn replace_nodes(inst: &mut InstBundle) { } -fn verify_args(args: &InstArgs) -> anyhow::Result<()> { - anyhow::ensure!(args.node != args.new_node, - "node and new_node must be different."); - Ok(()) +fn verify_args(args: &InstArgs) -> ArgResult { + if args.node == args.new_node { + return ArgResult::error("node and new_node must be different."); + } + + ArgResult::Ok } diff --git a/src/commands/set_param2.rs b/src/commands/set_param2.rs index bf00fe2..a1eb48c 100644 --- a/src/commands/set_param2.rs +++ b/src/commands/set_param2.rs @@ -1,4 +1,4 @@ -use super::Command; +use super::{Command, ArgResult}; use crate::unwrap_or; use crate::spatial::{Vec3, Area, InverseBlockIterator}; @@ -107,10 +107,12 @@ fn set_param2(inst: &mut InstBundle) { } -fn verify_args(args: &InstArgs) -> anyhow::Result<()> { - anyhow::ensure!(args.area.is_some() || args.node.is_some(), - "An area and/or node must be provided."); - Ok(()) +fn verify_args(args: &InstArgs) -> ArgResult { + if args.area.is_none() && args.node.is_none() { + return ArgResult::error("An area and/or node must be provided."); + } + + ArgResult::Ok } diff --git a/src/instance.rs b/src/instance.rs index 9f7493d..b1e6f79 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -7,6 +7,7 @@ use anyhow::Context; use crate::spatial::{Vec3, Area, MAP_LIMIT}; use crate::map_database::MapDatabase; use crate::commands; +use crate::commands::ArgResult; #[derive(Clone)] @@ -32,6 +33,7 @@ pub enum ArgType { #[derive(Debug)] pub struct InstArgs { + pub do_confirmation: bool, pub command: String, pub map_path: String, pub input_map_path: Option, @@ -86,6 +88,7 @@ impl InstStatus { pub enum LogType { Info, + Warning, Error } @@ -93,22 +96,29 @@ impl std::fmt::Display for LogType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Info => write!(f, "info"), + Self::Warning => write!(f, "warning"), Self::Error => write!(f, "error") } } } -pub enum InstEvent { +pub enum ServerEvent { + Log(LogType, String), NewState(InstState), - Log(LogType, String) + ConfirmRequest, +} + + +pub enum ClientEvent { + ConfirmResponse(bool), } -#[derive(Clone)] pub struct StatusServer { status: Arc>, - event_tx: mpsc::Sender + event_tx: mpsc::Sender, + event_rx: mpsc::Receiver, } impl StatusServer { @@ -118,7 +128,7 @@ impl StatusServer { pub fn set_state(&self, new_state: InstState) { self.status.lock().unwrap().state = new_state; - self.event_tx.send(InstEvent::NewState(new_state)).unwrap(); + self.event_tx.send(ServerEvent::NewState(new_state)).unwrap(); } pub fn set_total(&self, total: usize) { @@ -146,8 +156,18 @@ impl StatusServer { self.set_state(InstState::Ignore); } - pub fn log>(&self, lt: LogType, msg: S) { - self.event_tx.send(InstEvent::Log(lt, msg.as_ref().to_string())) + pub fn get_confirmation(&self) -> bool { + self.event_tx.send(ServerEvent::ConfirmRequest).unwrap(); + while let Ok(event) = self.event_rx.recv() { + match event { + ClientEvent::ConfirmResponse(res) => return res + } + } + false + } + + fn log>(&self, lt: LogType, msg: S) { + self.event_tx.send(ServerEvent::Log(lt, msg.as_ref().to_string())) .unwrap(); } @@ -155,6 +175,10 @@ impl StatusServer { self.log(LogType::Info, msg); } + pub fn log_warning>(&self, msg: S) { + self.log(LogType::Warning, msg); + } + pub fn log_error>(&self, msg: S) { self.log(LogType::Error, msg); } @@ -162,14 +186,44 @@ impl StatusServer { pub struct StatusClient { - pub event_rx: mpsc::Receiver, - status: Arc> + status: Arc>, + event_tx: mpsc::Sender, + event_rx: mpsc::Receiver, } impl StatusClient { - pub fn get(&self) -> InstStatus { + pub fn get_status(&self) -> InstStatus { self.status.lock().unwrap().clone() } + + #[inline] + pub fn receiver(&self) -> &mpsc::Receiver { + &self.event_rx + } + + pub fn confirm(&self, choice: bool) { + self.event_tx.send(ClientEvent::ConfirmResponse(choice)).unwrap(); + } +} + + +fn status_link() -> (StatusServer, StatusClient) { + let status1 = Arc::new(Mutex::new(InstStatus::new())); + let status2 = status1.clone(); + let (s_event_tx, s_event_rx) = mpsc::channel(); + let (c_event_tx, c_event_rx) = mpsc::channel(); + ( + StatusServer { + status: status1, + event_tx: s_event_tx, + event_rx: c_event_rx, + }, + StatusClient { + status: status2, + event_tx: c_event_tx, + event_rx: s_event_rx, + } + ) } @@ -181,17 +235,6 @@ pub struct InstBundle<'a> { } -fn status_channel() -> (StatusServer, StatusClient) { - let status1 = Arc::new(Mutex::new(InstStatus::new())); - let status2 = status1.clone(); - let (event_tx, event_rx) = mpsc::channel(); - ( - StatusServer {status: status1, event_tx}, - StatusClient {status: status2, event_rx} - ) -} - - fn verify_args(args: &InstArgs) -> anyhow::Result<()> { // TODO: Complete verifications. @@ -280,14 +323,17 @@ fn open_map(path: PathBuf, flags: sqlite::OpenFlags) } -fn compute_thread(args: InstArgs, status: StatusServer) - -> anyhow::Result<()> -{ +fn compute_thread(args: InstArgs, status: StatusServer) -> anyhow::Result<()> { verify_args(&args)?; let commands = commands::get_commands(); + let mut cmd_warning = None; if let Some(cmd_verify) = commands[args.command.as_str()].verify_args { - cmd_verify(&args)? + cmd_warning = match cmd_verify(&args) { + ArgResult::Ok => None, + ArgResult::Warning(w) => Some(w), + ArgResult::Error(e) => anyhow::bail!(e) + } } let db_conn = open_map(PathBuf::from(&args.map_path), @@ -303,14 +349,27 @@ fn compute_thread(args: InstArgs, status: StatusServer) Some(conn) => Some(MapDatabase::new(conn)?), None => None }; - // TODO: Standard warning? + let func = commands[args.command.as_str()].func; let mut inst = InstBundle {args, status, db, idb}; - func(&mut inst); + + // Issue warnings and confirmation prompt. + if inst.args.do_confirmation { + inst.status.log_warning( + "This tool can permanently damage your Minetest world.\n\ + Always EXIT Minetest and BACK UP the map database before use."); + } + if let Some(w) = cmd_warning { + inst.status.log_warning(w); + } + if inst.args.do_confirmation && !inst.status.get_confirmation() { + return Ok(()); + } + + func(&mut inst); // The real thing! let fails = inst.status.get_status().blocks_failed; if fails > 0 { - // TODO: log_warning inst.status.log_info(format!( "Skipped {} invalid/unsupported mapblocks.", fails)); } @@ -327,16 +386,18 @@ fn compute_thread(args: InstArgs, status: StatusServer) pub fn spawn_compute_thread(args: InstArgs) -> (std::thread::JoinHandle<()>, StatusClient) { - let (status_tx, status_rx) = status_channel(); + let (status_server, status_client) = status_link(); // Clone within this thread to avoid issue #39364 (hopefully). - let status_tx_2 = status_tx.clone(); + let raw_event_tx = status_server.event_tx.clone(); let h = std::thread::Builder::new() .name("compute".to_string()) .spawn(move || { - compute_thread(args, status_tx_2).unwrap_or_else( - |err| status_tx.log_error(&err.to_string()) + compute_thread(args, status_server).unwrap_or_else( + // TODO: Find a cleaner way to do this. + |err| raw_event_tx.send( + ServerEvent::Log(LogType::Error, err.to_string())).unwrap() ); }) .unwrap(); - (h, status_rx) + (h, status_client) }