Completion Part 2

master
random-geek 2021-03-20 23:44:06 -07:00
parent a593f141ff
commit b1f3f66006
12 changed files with 220 additions and 101 deletions

View File

@ -20,11 +20,12 @@ SQLite format maps are currently supported.
## General usage
`mapeditr [-h] <map> <subcommand>`
`mapeditr [-h] [-y] <map> <subcommand>`
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).
- `<map>`: 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

View File

@ -139,6 +139,12 @@ fn parse_cmd_line_args() -> anyhow::Result<InstArgs> {
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<InstArgs> {
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:<total_bars$}] {progress:.1}% | {elapsed} elapsed \
| {remaining} remaining",
bars=bars,
@ -228,7 +235,7 @@ fn print_editing_status(done: usize, total: usize, real_start: Instant,
}
);
} else {
eprint!("\rProcessing... {} elapsed", fmt_duration(real_elapsed));
print!("\rProcessing... {} elapsed", fmt_duration(real_elapsed));
}
std::io::stdout().flush().unwrap();
@ -239,13 +246,21 @@ fn print_log(log_type: LogType, msg: String) {
let prefix = format!("{}: ", log_type);
let indented = msg.lines().collect::<Vec<_>>()
.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);
}
}

View File

@ -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
}

View File

@ -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."
}

View File

@ -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."
}
}

View File

@ -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() {

View File

@ -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<fn(&InstArgs) -> anyhow::Result<()>>,
pub verify_args: Option<fn(&InstArgs) -> ArgResult>,
pub help: &'static str,
pub args: Vec<(ArgType, &'static str)>
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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<String>,
@ -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<Mutex<InstStatus>>,
event_tx: mpsc::Sender<InstEvent>
event_tx: mpsc::Sender<ServerEvent>,
event_rx: mpsc::Receiver<ClientEvent>,
}
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<S: AsRef<str>>(&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<S: AsRef<str>>(&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<S: AsRef<str>>(&self, msg: S) {
self.log(LogType::Warning, msg);
}
pub fn log_error<S: AsRef<str>>(&self, msg: S) {
self.log(LogType::Error, msg);
}
@ -162,14 +186,44 @@ impl StatusServer {
pub struct StatusClient {
pub event_rx: mpsc::Receiver<InstEvent>,
status: Arc<Mutex<InstStatus>>
status: Arc<Mutex<InstStatus>>,
event_tx: mpsc::Sender<ClientEvent>,
event_rx: mpsc::Receiver<ServerEvent>,
}
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<ServerEvent> {
&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)
}