Completion Part 1

master
random-geek 2021-03-20 00:27:53 -07:00
parent 0ce828b6fc
commit a593f141ff
18 changed files with 451 additions and 278 deletions

View File

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

View File

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

View File

@ -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::<u16, Vec<u8>>::new();
let mut new_nimap = BTreeMap::new();
let mut map = vec![0u16; id_count];
for id in 0..id_count {
// Skip unused IDs.

View File

@ -26,9 +26,9 @@ fn arg_to_pos(p: clap::Values) -> anyhow::Result<Vec3> {
fn to_cmd_line_args<'a>(tup: &(ArgType, &'a str))
-> Vec<Arg<'a, 'a>>
{
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<InstArgs> {
let app = App::new("MapEditr")
.about("Edits Minetest worlds/map databases.")
.after_help("For command-specific help, run: mapeditr <command> -h")
.after_help(
"For command-specific help, run: mapeditr <SUBCOMMAND> -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;

View File

@ -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<Result<MapBlock, MapBlockError>>;
@ -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."
}
}

View File

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

View File

@ -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();
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Area>,
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)));
}

View File

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

View File

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

View File

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

View File

@ -80,6 +80,22 @@ impl Area {
(self.max.z - self.min.z + 1) as u64
}
pub fn intersection(&self, rhs: Self) -> Option<Self> {
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));

View File

@ -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<Self::Item> {
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);
}
}
}
}
}

View File

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