359 lines
9.4 KiB
Rust
359 lines
9.4 KiB
Rust
use std::io::prelude::*;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use clap::{App, Arg, SubCommand, AppSettings, crate_version, crate_authors};
|
|
use anyhow::Context;
|
|
|
|
use crate::spatial::{Vec3, Area};
|
|
use crate::instance::{LogType, ArgType, InstArgs};
|
|
use crate::commands::{get_commands};
|
|
use crate::utils::fmt_duration;
|
|
|
|
|
|
fn arg_to_pos(p: clap::Values) -> anyhow::Result<Vec3> {
|
|
let vals: Vec<_> = p.collect();
|
|
if vals.len() != 3 {
|
|
anyhow::bail!("");
|
|
}
|
|
Ok(Vec3::new(
|
|
vals[0].parse()?,
|
|
vals[1].parse()?,
|
|
vals[2].parse()?
|
|
))
|
|
}
|
|
|
|
|
|
fn to_cmd_line_args<'a>(tup: &(ArgType, &'a str))
|
|
-> Vec<Arg<'a, 'a>>
|
|
{
|
|
let arg_type = tup.0.clone();
|
|
let help_msg = tup.1;
|
|
if let ArgType::Area(req) = arg_type {
|
|
return vec![
|
|
Arg::with_name("p1")
|
|
.long("p1")
|
|
.allow_hyphen_values(true)
|
|
.number_of_values(3)
|
|
.value_names(&["x", "y", "z"])
|
|
.required(req)
|
|
.requires("p2")
|
|
.help(help_msg),
|
|
Arg::with_name("p2")
|
|
.long("p2")
|
|
.allow_hyphen_values(true)
|
|
.number_of_values(3)
|
|
.value_names(&["x", "y", "z"])
|
|
.required(req)
|
|
.requires("p1")
|
|
.help(help_msg)
|
|
];
|
|
}
|
|
|
|
let arg = match arg_type {
|
|
ArgType::Area(_) => unreachable!(),
|
|
ArgType::InputMapPath =>
|
|
Arg::with_name("input_map")
|
|
.required(true),
|
|
ArgType::Invert =>
|
|
Arg::with_name("invert")
|
|
.long("invert"),
|
|
ArgType::Offset(req) =>
|
|
Arg::with_name("offset")
|
|
.long("offset")
|
|
.allow_hyphen_values(true)
|
|
.number_of_values(3)
|
|
.value_names(&["x", "y", "z"])
|
|
.required(req),
|
|
ArgType::Node(req) => {
|
|
let a = Arg::with_name("node");
|
|
if req {
|
|
a.required(true)
|
|
} else {
|
|
a.long("node").takes_value(true)
|
|
}
|
|
},
|
|
ArgType::Nodes =>
|
|
Arg::with_name("nodes")
|
|
.long("nodes")
|
|
.min_values(1),
|
|
ArgType::NewNode =>
|
|
Arg::with_name("new_node")
|
|
.takes_value(true)
|
|
.required(true),
|
|
ArgType::Object =>
|
|
Arg::with_name("object")
|
|
.long("obj")
|
|
.takes_value(true),
|
|
ArgType::Item =>
|
|
Arg::with_name("item")
|
|
.takes_value(true)
|
|
.required(true),
|
|
ArgType::Items =>
|
|
Arg::with_name("items")
|
|
.long("items")
|
|
.min_values(0),
|
|
ArgType::NewItem =>
|
|
Arg::with_name("new_item")
|
|
.takes_value(true),
|
|
ArgType::Delete =>
|
|
Arg::with_name("delete")
|
|
.long("delete"),
|
|
ArgType::DeleteMeta =>
|
|
Arg::with_name("delete_meta")
|
|
.long("deletemeta"),
|
|
ArgType::Key =>
|
|
Arg::with_name("key")
|
|
.takes_value(true)
|
|
.required(true),
|
|
ArgType::Value =>
|
|
Arg::with_name("value")
|
|
.takes_value(true),
|
|
ArgType::Param2 =>
|
|
Arg::with_name("param2")
|
|
.required(true),
|
|
}.help(help_msg);
|
|
|
|
vec![arg]
|
|
}
|
|
|
|
|
|
fn parse_cmd_line_args() -> anyhow::Result<InstArgs> {
|
|
/* Create the clap app */
|
|
let commands = get_commands();
|
|
|
|
let app_commands = commands.iter().map(|(cmd_name, cmd)| {
|
|
let args: Vec<_> = cmd.args.iter().flat_map(to_cmd_line_args)
|
|
.collect();
|
|
SubCommand::with_name(cmd_name)
|
|
.about(cmd.help)
|
|
.args(&args)
|
|
.after_help("For additional information, see the manual.")
|
|
});
|
|
|
|
let app = App::new("MapEditr")
|
|
.about("Edits Minetest worlds/map databases.")
|
|
.after_help(
|
|
"For command-specific help, run: mapeditr <SUBCOMMAND> -h\n\
|
|
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.")
|
|
)
|
|
.arg(Arg::with_name("map")
|
|
.required(true)
|
|
.help("Path to world directory or map database to edit")
|
|
)
|
|
.setting(AppSettings::SubcommandRequired)
|
|
.subcommands(app_commands);
|
|
|
|
/* Parse the arguments */
|
|
let matches = app.get_matches();
|
|
let sub_name = matches.subcommand_name().unwrap().to_string();
|
|
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),
|
|
area: {
|
|
let p1_maybe = sub_matches.values_of("p1").map(arg_to_pos)
|
|
.transpose().context("Invalid p1 value.")?;
|
|
let p2_maybe = sub_matches.values_of("p2").map(arg_to_pos)
|
|
.transpose().context("Invalid p2 value.")?;
|
|
if let (Some(p1), Some(p2)) = (p1_maybe, p2_maybe) {
|
|
Some(Area::from_unsorted(p1, p2))
|
|
} else {
|
|
None
|
|
}
|
|
},
|
|
invert: sub_matches.is_present("invert"),
|
|
offset: sub_matches.values_of("offset").map(arg_to_pos).transpose()
|
|
.context("Invalid offset value.")?,
|
|
node: sub_matches.value_of("node").map(str::to_string),
|
|
nodes: sub_matches.values_of("nodes").iter_mut().flatten()
|
|
.map(str::to_string).collect(),
|
|
new_node: sub_matches.value_of("new_node").map(str::to_string),
|
|
object: sub_matches.value_of("object").map(str::to_string),
|
|
item: sub_matches.value_of("item").map(str::to_string),
|
|
items: sub_matches.values_of("items")
|
|
.map(|v| v.map(str::to_string).collect()),
|
|
new_item: sub_matches.value_of("new_item").map(str::to_string),
|
|
delete: sub_matches.is_present("delete"),
|
|
delete_meta: sub_matches.is_present("delete_meta"),
|
|
key: sub_matches.value_of("key").map(str::to_string),
|
|
value: sub_matches.value_of("value").map(str::to_string),
|
|
param2: sub_matches.value_of("param2_val").map(|val| val.parse())
|
|
.transpose().context("Invalid param2 value.")?,
|
|
})
|
|
}
|
|
|
|
|
|
fn print_editing_status(done: usize, total: usize, real_start: Instant,
|
|
eta_start: Instant, show_progress: bool)
|
|
{
|
|
let now = Instant::now();
|
|
let real_elapsed = now.duration_since(real_start);
|
|
|
|
if show_progress {
|
|
let eta_elapsed = now.duration_since(eta_start);
|
|
let progress = match total {
|
|
0 => 0.,
|
|
_ => done as f32 / total as f32
|
|
};
|
|
|
|
let remaining = if progress >= 0.1 {
|
|
Some(Duration::from_secs_f32(
|
|
eta_elapsed.as_secs_f32() / progress * (1. - progress)
|
|
))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
const TOTAL_BARS: usize = 25;
|
|
let num_bars = (progress * TOTAL_BARS as f32) as usize;
|
|
let bars = "=".repeat(num_bars);
|
|
|
|
print!(
|
|
"\r[{bars:<total_bars$}] {progress:.1}% | {elapsed} elapsed \
|
|
| {remaining} remaining",
|
|
bars=bars,
|
|
total_bars=TOTAL_BARS,
|
|
progress=progress * 100.,
|
|
elapsed=fmt_duration(real_elapsed),
|
|
remaining=if let Some(d) = remaining {
|
|
fmt_duration(d)
|
|
} else {
|
|
String::from("--:--")
|
|
}
|
|
);
|
|
} else {
|
|
print!("\rProcessing... {} elapsed", fmt_duration(real_elapsed));
|
|
}
|
|
|
|
std::io::stdout().flush().unwrap();
|
|
}
|
|
|
|
|
|
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()) ));
|
|
println!("{}{}", prefix, indented);
|
|
}
|
|
|
|
|
|
fn get_confirmation() -> bool {
|
|
print!("Proceed? (Y/n): ");
|
|
std::io::stdout().flush().unwrap();
|
|
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, ServerEvent, spawn_compute_thread};
|
|
|
|
let args = match parse_cmd_line_args() {
|
|
Ok(a) => a,
|
|
Err(e) => {
|
|
print_log(LogType::Error, e.to_string());
|
|
return;
|
|
}
|
|
};
|
|
let (handle, status) = spawn_compute_thread(args);
|
|
|
|
const TICK: Duration = Duration::from_millis(25);
|
|
const UPDATE_INTERVAL: Duration = Duration::from_millis(500);
|
|
|
|
let mut last_update = Instant::now();
|
|
let mut querying_start = last_update;
|
|
let mut editing_start = last_update;
|
|
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.receiver().recv_timeout(TICK) {
|
|
Ok(event) => match event {
|
|
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) !=
|
|
(new_state == InstState::Ignore)
|
|
{
|
|
forced_update =
|
|
if cur_state == InstState::Ignore { new_state }
|
|
else { cur_state };
|
|
}
|
|
if new_state == InstState::Querying {
|
|
// Store time for determining elapsed time.
|
|
querying_start = now;
|
|
} else if new_state == InstState::Editing {
|
|
// Store start time for determining ETA.
|
|
editing_start = now;
|
|
}
|
|
cur_state = new_state;
|
|
},
|
|
ServerEvent::ConfirmRequest => {
|
|
newline_if(&mut need_newline);
|
|
status.send_confirmation(get_confirmation());
|
|
},
|
|
},
|
|
Err(err) => {
|
|
// Compute thread has exited; break out of the loop.
|
|
if err == mpsc::RecvTimeoutError::Disconnected {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
let timed_update_ready = now >= last_update + UPDATE_INTERVAL;
|
|
|
|
if forced_update == InstState::Querying
|
|
|| (cur_state == InstState::Querying && timed_update_ready)
|
|
{
|
|
print!("\rQuerying mapblocks... {} found.",
|
|
status.get_status().blocks_total);
|
|
std::io::stdout().flush().unwrap();
|
|
last_update = now;
|
|
need_newline = true;
|
|
}
|
|
else if forced_update == InstState::Editing
|
|
|| (cur_state == InstState::Editing && timed_update_ready)
|
|
{
|
|
let s = status.get_status();
|
|
print_editing_status(s.blocks_done, s.blocks_total,
|
|
querying_start, editing_start, s.show_progress);
|
|
last_update = now;
|
|
need_newline = true;
|
|
}
|
|
|
|
// Print a newline after the last querying/editing message.
|
|
if cur_state == InstState::Ignore {
|
|
newline_if(&mut need_newline);
|
|
}
|
|
}
|
|
|
|
let _ = handle.join();
|
|
}
|