From a593f141ff7e57c93fd167e9e00012941c9b79ac Mon Sep 17 00:00:00 2001 From: random-geek <35757396+random-geek@users.noreply.github.com> Date: Sat, 20 Mar 2021 00:27:53 -0700 Subject: [PATCH] Completion Part 1 --- Manual.md | 24 +++---- README.md | 39 +++++----- src/block_utils.rs | 4 +- src/cmd_line.rs | 77 +++++++++----------- src/commands/clone.rs | 49 +++++++------ src/commands/delete_blocks.rs | 8 +-- src/commands/delete_meta.rs | 13 ++-- src/commands/delete_objects.rs | 2 +- src/commands/delete_timers.rs | 2 +- src/commands/fill.rs | 51 +++++++------ src/commands/overlay.rs | 27 +++++-- src/commands/replace_nodes.rs | 127 ++++++++++++++------------------- src/commands/set_meta_var.rs | 1 + src/commands/set_param2.rs | 88 ++++++++++++----------- src/instance.rs | 55 ++++++++------ src/spatial/area.rs | 46 ++++++++++++ src/spatial/mod.rs | 109 +++++++++++++++++++++++++++- src/spatial/vec3.rs | 7 +- 18 files changed, 451 insertions(+), 278 deletions(-) diff --git a/Manual.md b/Manual.md index bb7fd93..f8b321c 100644 --- a/Manual.md +++ b/Manual.md @@ -15,8 +15,8 @@ already be generated. This can be done by either exploring the area in-game, or by using Minetest's built-in `/emergeblocks` command. MapEditr supports all maps created since Minetest version 0.4.2-rc1, released -July 2012. Any unsupported areas of the map will be skipped (TODO). Note that -only SQLite format maps are currently supported. +July 2012. Any unsupported areas of the map will be skipped. Note that only +SQLite format maps are currently supported. ## General usage @@ -53,7 +53,7 @@ WorldEdit `//fixlight` command. Usage: `clone --p1 x y z --p2 x y z --offset x y z` -Clone (copy) a given area to a new location. +Clone (copy) the contents of an area to a new location. Arguments: @@ -69,14 +69,14 @@ from or into mapblocks that are not yet generated. Usage: `deleteblocks --p1 x y z --p2 x y z [--invert]` -Deletes all mapblocks in the given area. +Delete all mapblocks inside or outside an area. Arguments: -- `--p1, --p2`: Area to delete from. Only mapblocks fully inside this area -will be deleted. -- `--invert`: Delete only mapblocks that are fully *outside* the given -area. +- `--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! **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 @@ -86,13 +86,13 @@ terrain glitches. Usage: `deletemeta [--node ] [--p1 x y z] [--p2 x y z] [--invert]` -Delete metadata of a certain node and/or within a certain area. This includes -node inventories as well. +Delete node metadata of certain nodes. Node inventories (such as chest/furnace +contents) are also deleted. Arguments: -- `--node`: Name of node to modify. If not specified, the metadata of all -nodes will be deleted. +- `--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. diff --git a/README.md b/README.md index 16cd235..52205c9 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,35 @@ # MapEditr -TODO: Add a license. - -MapEditr is a command-line tool for relatively fast manipulation of Minetest -worlds. It can replace nodes, fill areas, combine parts of different worlds, -and much more. +MapEditr is a command-line tool for fast manipulation of Minetest worlds. It +can replace nodes and items, fill areas, combine parts of different worlds, and +much more. This tool is functionally similar to [WorldEdit][1], but designed for large -operations that would be impractical to do using WorldEdit. Since it is mainly -optimized for speed, MapEditr is not as full-featured as in-game world editors -such as WorldEdit. +operations that would be impractical to do within Minetest. Since it is mainly +optimized for speed, MapEditr lacks some of the more specialty features of +WorldEdit. -MapEditr is originally based on [MapEdit][2], but rewritten in Rust, hence the -added "r". Switching to a compiled language will make MapEditr more robust and -easier to maintain in the future. +MapEditr was originally based on [MapEdit][2], except written in Rust rather +than Python (hence the added "r"). Switching to a compiled language will make +MapEditr more robust and easier to maintain in the future. [1]: https://github.com/Uberi/Minetest-WorldEdit [2]: https://github.com/random-geek/MapEdit -## Installation +## Compilation/Installation TODO: Pre-built binaries To compile from source, you must have Rust installed first, which can be -downloaded from [here][3]. Then, in the MapEditr directory, run: +downloaded from [the Rust website][3]. Then, in the MapEditr directory, simply +run: `cargo build --release` -The `--release` flag is important, as it optimizes the generated executable, -making it much faster. +The `--release` flag is important, as it produces an optimized executable which +runs much faster than the default, unoptimized version. -[3]: https://www.rust-lang.org/tools/install +[3]: https://www.rust-lang.org ## Usage @@ -43,10 +42,14 @@ Some useful things you can do with MapEditr: - Build extremely long walls and roads in seconds using `fill`. - Combine multiple worlds or map saves with `overlay`. +## License + +TODO + ## Acknowledgments -The [Minetest][4] project has been rather important for the making of MapEdit/ -MapEditr, for obvious reasons. +The [Minetest][4] project has been rather important for the making of +MapEdit/MapEditr, for obvious reasons. Some parts of the original MapEdit code were adapted from AndrejIT's [map_unexplore][5] project. All due credit goes to the author(s) of that diff --git a/src/block_utils.rs b/src/block_utils.rs index 1618533..124f07d 100644 --- a/src/block_utils.rs +++ b/src/block_utils.rs @@ -1,4 +1,4 @@ -// TODO: Move this file somewhere else. +// TODO: Move this file somewhere else? use std::collections::BTreeMap; use crate::map_block::{MapBlock, NodeMetadataList}; @@ -115,7 +115,7 @@ pub fn clean_name_id_map(block: &mut MapBlock) { } // Rebuild the name-ID map. - let mut new_nimap = BTreeMap::>::new(); + let mut new_nimap = BTreeMap::new(); let mut map = vec![0u16; id_count]; for id in 0..id_count { // Skip unused IDs. diff --git a/src/cmd_line.rs b/src/cmd_line.rs index 5e7f2be..b35f708 100644 --- a/src/cmd_line.rs +++ b/src/cmd_line.rs @@ -26,9 +26,9 @@ fn arg_to_pos(p: clap::Values) -> anyhow::Result { fn to_cmd_line_args<'a>(tup: &(ArgType, &'a str)) -> Vec> { - let arg = tup.0.clone(); - let help = tup.1; - if let ArgType::Area(req) = arg { + 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") @@ -37,7 +37,7 @@ fn to_cmd_line_args<'a>(tup: &(ArgType, &'a str)) .value_names(&["x", "y", "z"]) .required(req) .requires("p2") - .help(help), + .help(help_msg), Arg::with_name("p2") .long("p2") .allow_hyphen_values(true) @@ -45,91 +45,78 @@ fn to_cmd_line_args<'a>(tup: &(ArgType, &'a str)) .value_names(&["x", "y", "z"]) .required(req) .requires("p1") - .help(help) + .help(help_msg) ]; } - // TODO: Help is redundant. - vec![match arg { + // TODO: Ensure arguments are correctly defined. + let arg = match arg_type { + ArgType::Area(_) => unreachable!(), ArgType::InputMapPath => Arg::with_name("input_map") - .required(true) - .help(help), - ArgType::Area(_) => unreachable!(), + .required(true), ArgType::Invert => Arg::with_name("invert") - .long("invert") - .help(help), + .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) - .help(help), + .required(req), ArgType::Node(req) => { let a = Arg::with_name("node") - .required(req) - .help(help); - if !req { - a.long("node").takes_value(true) - } else { + .required(req); + if req { a + } else { + a.long("node").takes_value(true) } }, ArgType::Nodes => Arg::with_name("nodes") .long("nodes") - .min_values(1) - .help(help), + .min_values(1), ArgType::NewNode => Arg::with_name("new_node") .takes_value(true) - .required(true) - .help(help), + .required(true), ArgType::Object => Arg::with_name("object") .long("obj") - .takes_value(true) - .help(help), + .takes_value(true), ArgType::Item => Arg::with_name("item") .takes_value(true) - .required(true) - .help(help), + .required(true), ArgType::Items => Arg::with_name("items") .long("items") .min_values(0) - .max_values(1) - .help(help), + .max_values(1), ArgType::NewItem => Arg::with_name("new_item") - .takes_value(true) - .help(help), + .takes_value(true), ArgType::DeleteMeta => Arg::with_name("delete_meta") - .long("deletemeta") - .help(help), + .long("deletemeta"), ArgType::DeleteItem => Arg::with_name("delete_item") - .long("delete") - .help(help), + .long("delete"), ArgType::Key => Arg::with_name("key") .takes_value(true) - .required(true) - .help(help), + .required(true), ArgType::Value => Arg::with_name("value") .takes_value(true) - .required(true) - .help(help), + .required(true), ArgType::Param2Val => Arg::with_name("param2_val") - .required(true) - .help(help), - }] + .required(true), + }.help(help_msg); + + vec![arg] } @@ -147,7 +134,9 @@ fn parse_cmd_line_args() -> anyhow::Result { let app = App::new("MapEditr") .about("Edits Minetest worlds/map databases.") - .after_help("For command-specific help, run: mapeditr -h") + .after_help( + "For command-specific help, run: mapeditr -h\n\ + For additional information, see the manual.") .version(crate_version!()) .author(crate_authors!()) // TODO: Move map arg to subcommands? @@ -322,7 +311,7 @@ pub fn run_cmd_line() { if forced_update == InstState::Querying || (cur_state == InstState::Querying && timed_update_ready) { - eprint!("\rQuerying map blocks... {} found.", + eprint!("\rQuerying mapblocks... {} found.", status.get().blocks_total); std::io::stdout().flush().unwrap(); last_update = now; diff --git a/src/commands/clone.rs b/src/commands/clone.rs index c1d53e8..9f4b21b 100644 --- a/src/commands/clone.rs +++ b/src/commands/clone.rs @@ -1,14 +1,29 @@ use super::{Command, BLOCK_CACHE_SIZE}; use crate::{unwrap_or, opt_unwrap_or}; -use crate::spatial::Vec3; +use crate::spatial::{Vec3, Area, MAP_LIMIT}; use crate::map_database::MapDatabase; use crate::map_block::{MapBlock, MapBlockError, is_valid_generated, NodeMetadataList, NodeMetadataListExt}; use crate::block_utils::{merge_blocks, merge_metadata, clean_name_id_map}; -use crate::instance::{ArgType, InstBundle}; +use crate::instance::{ArgType, InstBundle, InstArgs}; use crate::utils::{CacheMap, query_keys}; -use crate::time_keeper::TimeKeeper; + + +fn verify_args(args: &InstArgs) -> anyhow::Result<()> { + let map_area = Area::new( + Vec3::new(-MAP_LIMIT, -MAP_LIMIT, -MAP_LIMIT), + Vec3::new(MAP_LIMIT, MAP_LIMIT, MAP_LIMIT) + ); + + if map_area.intersection(args.area.unwrap() + args.offset.unwrap()) + .is_none() + { + anyhow::bail!("Destination area is outside map bounds."); + } + + Ok(()) +} type BlockResult = Option>; @@ -49,9 +64,8 @@ fn clone(inst: &mut InstBundle) { }); let mut block_cache = CacheMap::with_capacity(BLOCK_CACHE_SIZE); - let mut tk = TimeKeeper::new(); - inst.status.begin_editing(); + for dst_key in dst_keys { inst.status.inc_done(); @@ -93,40 +107,29 @@ fn clone(inst: &mut InstBundle) { let dst_frag_rel = (src_frag_abs + offset) .rel_block_overlap(dst_pos).unwrap(); - { - let _t = tk.get_timer("merge"); - merge_blocks(&src_block, &mut dst_block, - src_frag_rel, dst_frag_rel); - } - { - let _t = tk.get_timer("merge_meta"); - merge_metadata(&src_meta, &mut dst_meta, - src_frag_rel, dst_frag_rel); - } - } - - { - let _t = tk.get_timer("name-ID map cleanup"); - clean_name_id_map(&mut dst_block); + merge_blocks(&src_block, &mut dst_block, + src_frag_rel, dst_frag_rel); + merge_metadata(&src_meta, &mut dst_meta, + src_frag_rel, dst_frag_rel); } + clean_name_id_map(&mut dst_block); *dst_block.metadata.get_mut() = dst_meta.serialize(dst_block.version); inst.db.set_block(dst_key, &dst_block.serialize()).unwrap(); } inst.status.end_editing(); - tk.print(&inst.status); } pub fn get_command() -> Command { Command { func: clone, - verify_args: None, + verify_args: Some(verify_args), args: vec![ (ArgType::Area(true), "Area to clone"), (ArgType::Offset(true), "Vector to shift the area by") ], - help: "Clone (copy) a given area to a new location." + help: "Clone (copy) the contents of an area to a new location." } } diff --git a/src/commands/delete_blocks.rs b/src/commands/delete_blocks.rs index 9234259..ec91e85 100644 --- a/src/commands/delete_blocks.rs +++ b/src/commands/delete_blocks.rs @@ -10,7 +10,6 @@ fn delete_blocks(inst: &mut InstBundle) { inst.status.begin_editing(); for key in keys { - // TODO: This is kind of inefficient seeming. inst.status.inc_done(); inst.db.delete_block(key).unwrap(); } @@ -24,9 +23,10 @@ pub fn get_command() -> Command { func: delete_blocks, verify_args: None, args: vec![ - (ArgType::Area(true), "Area containing blocks to delete"), - (ArgType::Invert, "Delete all blocks *outside* the area") + (ArgType::Area(true), "Area containing mapblocks to delete"), + (ArgType::Invert, + "Delete all mapblocks fully *outside* the given area.") ], - help: "Delete all map blocks in a 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 f29f682..d9c9d77 100644 --- a/src/commands/delete_meta.rs +++ b/src/commands/delete_meta.rs @@ -11,7 +11,7 @@ fn delete_metadata(inst: &mut InstBundle) { let node = inst.args.node.as_ref().map(to_bytes); let keys = query_keys(&mut inst.db, &mut inst.status, - &to_slice(&node), inst.args.area, inst.args.invert, true); + to_slice(&node), inst.args.area, inst.args.invert, true); inst.status.begin_editing(); let mut count: u64 = 0; @@ -19,7 +19,8 @@ fn delete_metadata(inst: &mut InstBundle) { for key in keys { inst.status.inc_done(); let data = inst.db.get_block(key).unwrap(); - let mut block = unwrap_or!(MapBlock::deserialize(&data), continue); + let mut block = unwrap_or!(MapBlock::deserialize(&data), + { inst.status.inc_failed(); continue; }); let node_data = block.node_data.get_ref(); let node_id = node.as_deref().and_then(|n| block.nimap.get_id(n)); @@ -28,14 +29,14 @@ fn delete_metadata(inst: &mut InstBundle) { } let mut meta = unwrap_or!( - NodeMetadataList::deserialize(block.metadata.get_ref()), continue); + NodeMetadataList::deserialize(block.metadata.get_ref()), + { inst.status.inc_failed(); continue; }); let block_corner = Vec3::from_block_key(key) * 16; let mut to_delete = Vec::with_capacity(meta.len()); for (&idx, _) in &meta { - let pos = Vec3::from_u16_key(idx); - let abs_pos = pos + block_corner; + let abs_pos = Vec3::from_u16_key(idx) + block_corner; if let Some(a) = inst.args.area { if a.contains(abs_pos) == inst.args.invert { @@ -52,10 +53,10 @@ fn delete_metadata(inst: &mut InstBundle) { } if !to_delete.is_empty() { - count += to_delete.len() as u64; for idx in &to_delete { meta.remove(idx); } + count += to_delete.len() as u64; *block.metadata.get_mut() = meta.serialize(block.version); inst.db.set_block(key, &block.serialize()).unwrap(); } diff --git a/src/commands/delete_objects.rs b/src/commands/delete_objects.rs index ef74c27..5d4339e 100644 --- a/src/commands/delete_objects.rs +++ b/src/commands/delete_objects.rs @@ -65,7 +65,7 @@ fn delete_objects(inst: &mut InstBundle) { .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); + to_slice(&search_obj), inst.args.area, inst.args.invert, true); inst.status.begin_editing(); let mut count: u64 = 0; diff --git a/src/commands/delete_timers.rs b/src/commands/delete_timers.rs index 319c1ac..b209e76 100644 --- a/src/commands/delete_timers.rs +++ b/src/commands/delete_timers.rs @@ -11,7 +11,7 @@ fn delete_timers(inst: &mut InstBundle) { let node = inst.args.node.as_ref().map(to_bytes); let keys = query_keys(&mut inst.db, &mut inst.status, - &to_slice(&node), inst.args.area, inst.args.invert, true); + to_slice(&node), inst.args.area, inst.args.invert, true); inst.status.begin_editing(); let mut count: u64 = 0; diff --git a/src/commands/fill.rs b/src/commands/fill.rs index 54e9ef0..0f82ab7 100644 --- a/src/commands/fill.rs +++ b/src/commands/fill.rs @@ -1,21 +1,28 @@ use super::Command; use crate::unwrap_or; -use crate::spatial::{Vec3, Area}; +use crate::spatial::{Vec3, Area, InverseBlockIterator}; use crate::instance::{ArgType, InstBundle}; use crate::map_block::MapBlock; use crate::block_utils::clean_name_id_map; use crate::utils::{query_keys, to_bytes, fmt_big_num}; -fn fill_area(block: &mut MapBlock, area: Area, id: u16) { +fn fill_area(block: &mut MapBlock, id: u16, area: Area, invert: bool) { let nd = block.node_data.get_mut(); - for z in area.min.z ..= area.max.z { - let z_start = z * 256; - for y in area.min.y ..= area.max.y { - let zy_start = z_start + y * 16; - for x in area.min.x ..= area.max.x { - nd.nodes[(zy_start + x) as usize] = id; + + if invert { + for i in InverseBlockIterator::new(area) { + nd.nodes[i] = id; + } + } else { + for z in area.min.z ..= area.max.z { + let z_start = z * 256; + for y in area.min.y ..= area.max.y { + let zy_start = z_start + y * 16; + for x in area.min.x ..= area.max.x { + nd.nodes[(zy_start + x) as usize] = id; + } } } } @@ -27,7 +34,7 @@ fn fill(inst: &mut InstBundle) { let node = to_bytes(inst.args.new_node.as_ref().unwrap()); let keys = query_keys(&mut inst.db, &mut inst.status, - &[], Some(area), false, true); + &[], Some(area), inst.args.invert, true); inst.status.begin_editing(); @@ -42,24 +49,23 @@ fn fill(inst: &mut InstBundle) { continue; }); - if area.contains_block(pos) { - let nd = block.node_data.get_mut(); - for x in &mut nd.nodes { - *x = 0; - } - block.nimap.0.clear(); - block.nimap.0.insert(0, node.to_vec()); - count += nd.nodes.len() as u64; - } else { - let slice = area.rel_block_overlap(pos).unwrap(); + if area.contains_block(pos) != area.touches_block(pos) { + // Fill part of block + let block_part = area.rel_block_overlap(pos).unwrap(); let fill_id = block.nimap.get_id(&node).unwrap_or_else(|| { let next = block.nimap.get_max_id().unwrap() + 1; block.nimap.0.insert(next, node.to_vec()); next }); - fill_area(&mut block, slice, fill_id); + fill_area(&mut block, fill_id, block_part, inst.args.invert); clean_name_id_map(&mut block); - count += slice.volume(); + count += block_part.volume(); + } else { // Fill entire block + let nd = block.node_data.get_mut(); + nd.nodes.fill(0); + block.nimap.0.clear(); + block.nimap.0.insert(0, node.to_vec()); + count += nd.nodes.len() as u64; } inst.db.set_block(key, &block.serialize()).unwrap(); @@ -76,7 +82,8 @@ pub fn get_command() -> Command { verify_args: None, args: vec![ (ArgType::Area(true), "Area to fill"), - (ArgType::NewNode, "Name of node to fill area with") + (ArgType::NewNode, "Name of node to fill area with"), + (ArgType::Invert, "Fill all generated areas outside the area.") ], help: "Fill the entire area with one node." } diff --git a/src/commands/overlay.rs b/src/commands/overlay.rs index aa58fa6..b247bfc 100644 --- a/src/commands/overlay.rs +++ b/src/commands/overlay.rs @@ -1,7 +1,7 @@ use super::{Command, BLOCK_CACHE_SIZE}; use crate::{unwrap_or, opt_unwrap_or}; -use crate::spatial::{Vec3, Area}; +use crate::spatial::{Vec3, Area, MAP_LIMIT}; use crate::instance::{ArgType, InstArgs, InstBundle}; use crate::map_database::MapDatabase; use crate::map_block::{MapBlock, MapBlockError, is_valid_generated, @@ -11,11 +11,24 @@ use crate::utils::{query_keys, CacheMap}; fn verify_args(args: &InstArgs) -> anyhow::Result<()> { - let offset_if_nonzero = - args.offset.filter(|&off| off != Vec3::new(0, 0, 0)); - if args.invert && offset_if_nonzero.is_some() { + if args.invert + && args.offset.filter(|&ofs| ofs != Vec3::new(0, 0, 0)).is_some() + { anyhow::bail!("Inverted selections cannot be offset."); } + + let offset = args.offset.unwrap_or(Vec3::new(0, 0, 0)); + let map_area = Area::new( + Vec3::new(-MAP_LIMIT, -MAP_LIMIT, -MAP_LIMIT), + Vec3::new(MAP_LIMIT, MAP_LIMIT, MAP_LIMIT) + ); + + if map_area.intersection(args.area.unwrap_or(map_area) + offset) + .is_none() + { + anyhow::bail!("Destination area is outside map bounds."); + } + Ok(()) } @@ -45,12 +58,12 @@ fn overlay_no_offset(inst: &mut InstBundle) { if (!invert && area.contains_block(pos)) || (invert && !area.touches_block(pos)) - { // If possible, copy whole map block. + { // If possible, copy whole mapblock. let data = idb.get_block(key).unwrap(); if is_valid_generated(&data) { db.set_block(key, &data).unwrap(); } - } else { // Copy part of map block + } else { // Copy part of mapblock let res = || -> Result<(), MapBlockError> { let dst_data = opt_unwrap_or!( db.get_block(key).ok() @@ -91,7 +104,7 @@ fn overlay_no_offset(inst: &mut InstBundle) { } } } else { - // No area; copy whole map block. + // No area; copy whole mapblock. let data = idb.get_block(key).unwrap(); if is_valid_generated(&data) { db.set_block(key, &data).unwrap(); diff --git a/src/commands/replace_nodes.rs b/src/commands/replace_nodes.rs index dcd660a..179ec70 100644 --- a/src/commands/replace_nodes.rs +++ b/src/commands/replace_nodes.rs @@ -1,146 +1,127 @@ use super::Command; -use crate::spatial::{Vec3, Area}; +use crate::unwrap_or; +use crate::spatial::{Vec3, Area, InverseBlockIterator}; use crate::instance::{ArgType, InstArgs, InstBundle}; use crate::map_block::MapBlock; -use crate::time_keeper::TimeKeeper; use crate::utils::{query_keys, to_bytes, fmt_big_num}; fn do_replace( block: &mut MapBlock, key: i64, - search_id: u16, + old_id: u16, new_node: &[u8], area: Option, - invert: bool, - tk: &mut TimeKeeper + invert: bool ) -> u64 { let block_pos = Vec3::from_block_key(key); - let mut count = 0; + let mut replaced = 0; - // Replace nodes in a portion of a map block. - if area.is_some() && area.unwrap().contains_block(block_pos) != - area.unwrap().touches_block(block_pos) + // Replace nodes in a portion of a mapblock. + if area + .filter(|a| a.contains_block(block_pos) != a.touches_block(block_pos)) + .is_some() { - let _t = tk.get_timer("replace (partial block)"); let node_area = area.unwrap().rel_block_overlap(block_pos).unwrap(); - let mut new_replace_id = false; - let replace_id = block.nimap.get_id(new_node) - .unwrap_or_else(|| { - new_replace_id = true; - block.nimap.get_max_id().unwrap() + 1 - }); - - let mut idx = 0; - let mut old_node_present = false; - let mut new_node_present = false; - + let (new_id, new_id_needed) = match block.nimap.get_id(new_node) { + Some(id) => (id, false), + None => (block.nimap.get_max_id().unwrap() + 1, true) + }; let nd = block.node_data.get_mut(); - for z in 0..16 { - for y in 0..16 { - for x in 0..16 { - if nd.nodes[idx] == search_id - && node_area.contains(Vec3 {x, y, z}) != invert - { - nd.nodes[idx] = replace_id; - new_node_present = true; - count += 1; - } - if nd.nodes[idx] == search_id { - old_node_present = true; - } - idx += 1; + if invert { + for idx in InverseBlockIterator::new(node_area) { + if nd.nodes[idx] == old_id { + nd.nodes[idx] = new_id; + replaced += 1; + } + } + } else { + for pos in &node_area { + let idx = (pos.x + 16 * (pos.y + 16 * pos.z)) as usize; + if nd.nodes[idx] == old_id { + nd.nodes[idx] = new_id; + replaced += 1; } } } - // Replacement node not yet in name-ID map; insert it. - if new_replace_id && new_node_present { - block.nimap.0.insert(replace_id, new_node.to_vec()); + // If replacement ID is not in the name-ID map but was used, add it. + if new_id_needed && replaced > 0 { + block.nimap.0.insert(new_id, new_node.to_vec()); } - // Search node was completely eliminated; shift IDs down. - if !old_node_present { - for i in 0 .. nd.nodes.len() { - if nd.nodes[i] > search_id { - nd.nodes[i] -= 1; - } + // If all instances of the old ID were replaced, remove the old ID. + if !nd.nodes.contains(&old_id) { + for node in &mut nd.nodes { + *node -= (*node > old_id) as u16; } - block.nimap.remove_shift(search_id); + block.nimap.remove_shift(old_id); } } - // Replace nodes in whole map block. + // Replace nodes in whole mapblock. else { // Block already contains replacement node, beware! - if let Some(mut replace_id) = block.nimap.get_id(new_node) { - let _t = tk.get_timer("replace (non-unique replacement)"); + if let Some(mut new_id) = block.nimap.get_id(new_node) { // Delete unused ID from name-ID map and shift IDs down. - block.nimap.remove_shift(search_id); + block.nimap.remove_shift(old_id); // Shift replacement ID, if necessary. - replace_id -= (replace_id > search_id) as u16; + new_id -= (new_id > old_id) as u16; // Map old node IDs to new node IDs. let nd = block.node_data.get_mut(); for id in &mut nd.nodes { - *id = if *id == search_id { - count += 1; - replace_id + *id = if *id == old_id { + replaced += 1; + new_id } else { - *id - (*id > search_id) as u16 + *id - (*id > old_id) as u16 }; } } // Block does not contain replacement node. // Simply replace the node name in the name-ID map. else { - let _t = tk.get_timer("replace (unique replacement)"); let nd = block.node_data.get_ref(); for id in &nd.nodes { - count += (*id == search_id) as u64; + replaced += (*id == old_id) as u64; } - block.nimap.0.insert(search_id, new_node.to_vec()); + block.nimap.0.insert(old_id, new_node.to_vec()); } } - count + replaced } fn replace_nodes(inst: &mut InstBundle) { - let node = to_bytes(inst.args.node.as_ref().unwrap()); + let old_node = to_bytes(inst.args.node.as_ref().unwrap()); let new_node = to_bytes(inst.args.new_node.as_ref().unwrap()); let keys = query_keys(&mut inst.db, &inst.status, - std::slice::from_ref(&node), inst.args.area, inst.args.invert, true); + std::slice::from_ref(&old_node), + inst.args.area, inst.args.invert, true); inst.status.begin_editing(); let mut count = 0; - let mut tk = TimeKeeper::new(); for key in keys { let data = inst.db.get_block(key).unwrap(); - let mut block = { - let _t = tk.get_timer("decode"); - MapBlock::deserialize(&data).unwrap() - }; + let mut block = unwrap_or!(MapBlock::deserialize(&data), + { inst.status.inc_failed(); continue; }); - if let Some(search_id) = block.nimap.get_id(&node) { - count += do_replace(&mut block, key, search_id, &new_node, - inst.args.area, inst.args.invert, &mut tk); - let new_data = { - let _t = tk.get_timer("encode"); - block.serialize() - }; + if let Some(old_id) = block.nimap.get_id(&old_node) { + count += do_replace(&mut block, key, old_id, &new_node, + inst.args.area, inst.args.invert); + let new_data = block.serialize(); inst.db.set_block(key, &new_data).unwrap(); } inst.status.inc_done(); } - // tk.print(); inst.status.end_editing(); inst.status.log_info(format!("{} nodes replaced.", fmt_big_num(count))); } diff --git a/src/commands/set_meta_var.rs b/src/commands/set_meta_var.rs index da360e8..88a1364 100644 --- a/src/commands/set_meta_var.rs +++ b/src/commands/set_meta_var.rs @@ -8,6 +8,7 @@ use crate::utils::{query_keys, to_bytes, fmt_big_num}; fn set_meta_var(inst: &mut InstBundle) { + // TODO: Bytes input let key = to_bytes(inst.args.key.as_ref().unwrap()); let value = to_bytes(inst.args.value.as_ref().unwrap()); let nodes: Vec<_> = inst.args.nodes.iter().map(to_bytes).collect(); diff --git a/src/commands/set_param2.rs b/src/commands/set_param2.rs index fcd1305..bf00fe2 100644 --- a/src/commands/set_param2.rs +++ b/src/commands/set_param2.rs @@ -1,81 +1,89 @@ use super::Command; -use crate::spatial::{Vec3, Area}; +use crate::unwrap_or; +use crate::spatial::{Vec3, Area, InverseBlockIterator}; use crate::instance::{ArgType, InstArgs, InstBundle}; use crate::map_block::MapBlock; use crate::utils::{query_keys, to_bytes, to_slice, fmt_big_num}; -fn set_in_area_node(block: &mut MapBlock, area: Area, id: u16, val: u8) -> u64 +fn set_param2_partial(block: &mut MapBlock, area: Area, invert: bool, + node_id: Option, val: u8) -> u64 { let nd = block.node_data.get_mut(); let mut count = 0; - for z in area.min.z ..= area.max.z { - let z_start = z * 256; - for y in area.min.y ..= area.max.y { - let zy_start = z_start + y * 16; - for x in area.min.x ..= area.max.x { - let i = (zy_start + x) as usize; - if nd.nodes[i] == id { - nd.param2[i] = val; + + if invert { + if let Some(id) = node_id { + for idx in InverseBlockIterator::new(area) { + if nd.nodes[idx] == id { + nd.param2[idx] = val; count += 1; } } + } else { + for idx in InverseBlockIterator::new(area) { + nd.param2[idx] = val; + } + count += 4096 - area.volume(); + } + } else { + let no_node = node_id.is_none(); + let id = node_id.unwrap_or(0); + + for z in area.min.z ..= area.max.z { + let z_start = z * 256; + for y in area.min.y ..= area.max.y { + let zy_start = z_start + y * 16; + for x in area.min.x ..= area.max.x { + let i = (zy_start + x) as usize; + if no_node || nd.nodes[i] == id { + nd.param2[i] = val; + count += 1; + } + } + } } } + count } -fn set_in_area(block: &mut MapBlock, area: Area, val: u8) { - let nd = block.node_data.get_mut(); - for z in area.min.z ..= area.max.z { - let z_start = z * 256; - for y in area.min.y ..= area.max.y { - let zy_start = z_start + y * 16; - for x in area.min.x ..= area.max.x { - nd.param2[(zy_start + x) as usize] = val; - } - } - } -} - - fn set_param2(inst: &mut InstBundle) { let param2_val = inst.args.param2_val.unwrap(); let node = inst.args.node.as_ref().map(to_bytes); let keys = query_keys(&mut inst.db, &mut inst.status, - to_slice(&node), inst.args.area, false, true); + to_slice(&node), inst.args.area, inst.args.invert, true); inst.status.begin_editing(); let mut count: u64 = 0; + use crate::time_keeper::TimeKeeper; + let mut tk = TimeKeeper::new(); for key in keys { inst.status.inc_done(); let pos = Vec3::from_block_key(key); let data = inst.db.get_block(key).unwrap(); - let mut block = MapBlock::deserialize(&data).unwrap(); + let mut block = unwrap_or!(MapBlock::deserialize(&data), + { inst.status.inc_failed(); continue; }); let node_id = node.as_ref().and_then(|n| block.nimap.get_id(n)); if inst.args.node.is_some() && node_id.is_none() { - // Node not found in this map block. + // Node not found in this mapblock. continue; } let nd = block.node_data.get_mut(); if let Some(area) = inst.args.area - .filter(|a| !a.contains_block(pos)) + .filter(|a| a.contains_block(pos) != a.touches_block(pos)) { // Modify part of block - let overlap = area.rel_block_overlap(pos).unwrap(); - if let Some(nid) = node_id { - count += - set_in_area_node(&mut block, overlap, nid, param2_val); - } else { - set_in_area(&mut block, overlap, param2_val); - count += overlap.volume(); - } + let block_part = area.rel_block_overlap(pos).unwrap(); + let _t = tk.get_timer("set_param2_partial"); + count += set_param2_partial(&mut block, + block_part, inst.args.invert, node_id, param2_val); } else { // Modify whole block if let Some(nid) = node_id { for i in 0 .. nd.param2.len() { @@ -85,9 +93,7 @@ fn set_param2(inst: &mut InstBundle) { } } } else { - for x in &mut nd.param2 { - *x = param2_val; - } + nd.param2.fill(param2_val); count += nd.param2.len() as u64; } } @@ -96,6 +102,7 @@ fn set_param2(inst: &mut InstBundle) { } inst.status.end_editing(); + tk.print(&mut inst.status); inst.status.log_info(format!("{} nodes set.", fmt_big_num(count))); } @@ -113,9 +120,10 @@ pub fn get_command() -> Command { verify_args: Some(verify_args), args: vec![ (ArgType::Area(false), "Area in which to set param2 values"), + (ArgType::Invert, "Set param2 values outside the given area."), (ArgType::Node(false), "Node to set param2 values of"), (ArgType::Param2Val, "New param2 value") ], - help: "Set param2 values of an area or node." + help: "Set param2 value of certain nodes." } } diff --git a/src/instance.rs b/src/instance.rs index 286721a..9f7493d 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -4,7 +4,7 @@ use std::sync::mpsc; use anyhow::Context; -use crate::spatial::{Vec3, Area}; +use crate::spatial::{Vec3, Area, MAP_LIMIT}; use crate::map_database::MapDatabase; use crate::commands; @@ -193,6 +193,27 @@ fn status_channel() -> (StatusServer, StatusClient) { fn verify_args(args: &InstArgs) -> anyhow::Result<()> { + // TODO: Complete verifications. + + if args.area.is_none() && args.invert { + anyhow::bail!("Cannot invert without a specified area."); + } + if let Some(a) = args.area { + for pos in &[a.min, a.max] { + anyhow::ensure!(pos.is_valid_node_pos(), + "Area corner is outside map bounds: {}.", pos); + } + } + if let Some(offset) = args.offset { + let huge = |n| n < -MAP_LIMIT * 2 || n > MAP_LIMIT * 2; + + if huge(offset.x) || huge(offset.y) || huge(offset.z) { + anyhow::bail!( + "Offset cannot be larger than {} nodes in any direction.", + MAP_LIMIT * 2); + } + } + fn is_valid_name(name: &str) -> bool { if name == "air" || name == "ignore" { true @@ -205,29 +226,12 @@ fn verify_args(args: &InstArgs) -> anyhow::Result<()> { let mod_name = &name[..delim]; let item_name = &name[delim + 1..]; - if mod_name.find(|c: char| + mod_name.find(|c: char| !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')) - .is_some() - || item_name.find(|c: char| + .is_none() + && item_name.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_')) - .is_some() - { - false - } else { - true - } - } - } - - // TODO: Complete verifications. - - if args.area.is_none() && args.invert { - anyhow::bail!("Cannot invert without a specified area."); - } - if let Some(a) = args.area { - for pos in &[a.min, a.max] { - anyhow::ensure!(pos.is_valid_node_pos(), - "Area corner is outside map bounds: {}.", pos); + .is_none() } } @@ -246,6 +250,13 @@ fn verify_args(args: &InstArgs) -> anyhow::Result<()> { verify_name!(args.new_node, "Invalid node name: {}"); verify_name!(args.object, "Invalid object name: {}"); verify_name!(args.item, "Invalid item name: {}"); + if let Some(items) = &args.items { + for i in items { + anyhow::ensure!(is_valid_name(i), "Invalid item name: {}", i); + } + } + verify_name!(args.new_item, "Invalid item name: {}"); + // TODO: Are keys/values escaped? Ok(()) } diff --git a/src/spatial/area.rs b/src/spatial/area.rs index ee421dc..c7f5383 100644 --- a/src/spatial/area.rs +++ b/src/spatial/area.rs @@ -80,6 +80,22 @@ impl Area { (self.max.z - self.min.z + 1) as u64 } + pub fn intersection(&self, rhs: Self) -> Option { + let res = Self { + min: Vec3 { + x: max(self.min.x, rhs.min.x), + y: max(self.min.y, rhs.min.y), + z: max(self.min.z, rhs.min.z) + }, + max: Vec3 { + x: min(self.max.x, rhs.max.x), + y: min(self.max.y, rhs.max.y), + z: min(self.max.z, rhs.max.z) + } + }; + Some(res).filter(Self::is_valid) + } + pub fn contains(&self, pos: Vec3) -> bool { self.min.x <= pos.x && pos.x <= self.max.x && self.min.y <= pos.y && pos.y <= self.max.y @@ -253,6 +269,36 @@ mod tests { iter_area(Area::new(Vec3::new(0, -1, -2), Vec3::new(5, 7, 11))); } + #[test] + fn test_area_intersection() { + let triples = [ + ( + Area::new(Vec3::new(0, 0, 0), Vec3::new(0, 0, 0)), + Area::new(Vec3::new(1, 1, 0), Vec3::new(1, 1, 0)), + None + ), + ( + Area::new(Vec3::new(-10, -8, -10), Vec3::new(10, 8, 10)), + Area::new(Vec3::new(-12, 0, -2), Vec3::new(-8, 13, 2)), + Some(Area::new(Vec3::new(-10, 0, -2), Vec3::new(-8, 8, 2))) + ), + ( + Area::new(Vec3::new(0, 0, 0), Vec3::new(2, 2, 2)), + Area::new(Vec3::new(0, -1, 3), Vec3::new(2, 1, 5)), + None + ), + ( + Area::new(Vec3::new(0, -10, -10), Vec3::new(30, 30, 30)), + Area::new(Vec3::new(16, 16, -10), Vec3::new(29, 29, 20)), + Some(Area::new(Vec3::new(16, 16, -10), Vec3::new(29, 29, 20))) + ), + ]; + for t in &triples { + assert_eq!(t.0.intersection(t.1), t.2); + assert_eq!(t.1.intersection(t.0), t.2); + } + } + #[test] fn test_area_containment() { let area = Area::new(Vec3::new(-1, -32, 16), Vec3::new(30, -17, 54)); diff --git a/src/spatial/mod.rs b/src/spatial/mod.rs index 1bb1ee4..2f315f6 100644 --- a/src/spatial/mod.rs +++ b/src/spatial/mod.rs @@ -1,5 +1,112 @@ mod vec3; mod area; -pub use vec3::Vec3; +pub use vec3::{MAP_LIMIT, Vec3}; pub use area::Area; + + +/// Iterates over all the block indices that are *not* contained within an +/// area, in order. +pub struct InverseBlockIterator { + area: Area, + idx: usize, + can_skip: bool, + skip_pos: Vec3, + skip_idx: usize, + skip_len: usize, +} + +impl InverseBlockIterator { + pub fn new(area: Area) -> Self { + assert!(area.min.x >= 0 && area.max.x < 16 + && area.min.y >= 0 && area.max.y < 16 + && area.min.z >= 0 && area.max.z < 16); + + Self { + area, + idx: 0, + can_skip: true, + skip_pos: area.min, + skip_idx: + (area.min.x + area.min.y * 16 + area.min.z * 256) as usize, + skip_len: (area.max.x - area.min.x + 1) as usize + } + } +} + +impl Iterator for InverseBlockIterator { + type Item = usize; + + fn next(&mut self) -> Option { + while self.can_skip && self.idx >= self.skip_idx { + self.idx += self.skip_len; + // Increment self.skip_pos, self.skip_idx. + let mut sp = self.skip_pos; + sp.y += 1; + if sp.y > self.area.max.y { + sp.y = self.area.min.y; + sp.z += 1; + if sp.z > self.area.max.z { + // No more skips + self.can_skip = false; + break; + } + } + self.skip_pos = sp; + self.skip_idx = (sp.x + sp.y * 16 + sp.z * 256) as usize; + } + + if self.idx < 4096 { + let idx = self.idx; + self.idx += 1; + Some(idx) + } else { + None + } + } +} + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_inverse_block_iterator() { + let dim_pairs = [ + (1, 14), // Touching neither end + (1, 2), + (9, 9), + (1, 15), // Touching max end + (11, 15), + (15, 15), + (0, 0), // Touching min end + (0, 1), + (0, 14), + (0, 15), // End-to-end + ]; + + fn test_area(area: Area) { + let mut iter = InverseBlockIterator::new(area); + for pos in &Area::new(Vec3::new(0, 0, 0), Vec3::new(15, 15, 15)) { + if !area.contains(pos) { + let idx = (pos.x + pos.y * 16 + pos.z * 256) as usize; + assert_eq!(iter.next(), Some(idx)); + } + } + assert_eq!(iter.next(), None) + } + + for z_dims in &dim_pairs { + for y_dims in &dim_pairs { + for x_dims in &dim_pairs { + let area = Area::new( + Vec3::new(x_dims.0, y_dims.0, z_dims.0), + Vec3::new(x_dims.1, y_dims.1, z_dims.1) + ); + test_area(area); + } + } + } + } +} diff --git a/src/spatial/vec3.rs b/src/spatial/vec3.rs index 3b49be1..6e2c00b 100644 --- a/src/spatial/vec3.rs +++ b/src/spatial/vec3.rs @@ -1,3 +1,6 @@ +pub const MAP_LIMIT: i32 = 31000; + + #[derive(Clone, Copy, Debug, PartialEq)] pub struct Vec3 { pub x: i32, @@ -39,7 +42,7 @@ impl Vec3 { } pub fn is_valid_block_pos(&self) -> bool { - const LIMIT: i32 = 31000 / 16; + const LIMIT: i32 = MAP_LIMIT / 16; -LIMIT <= self.x && self.x <= LIMIT && -LIMIT <= self.y && self.y <= LIMIT @@ -47,7 +50,7 @@ impl Vec3 { } pub fn is_valid_node_pos(&self) -> bool { - const LIMIT: i32 = 31000; + const LIMIT: i32 = MAP_LIMIT; -LIMIT <= self.x && self.x <= LIMIT && -LIMIT <= self.y && self.y <= LIMIT