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. or by using Minetest's built-in `/emergeblocks` command.
MapEditr supports all maps created since Minetest version 0.4.2-rc1, released 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 July 2012. Any unsupported areas of the map will be skipped. Note that only
only SQLite format maps are currently supported. SQLite format maps are currently supported.
## General usage ## General usage
@ -53,7 +53,7 @@ WorldEdit `//fixlight` command.
Usage: `clone --p1 x y z --p2 x y z --offset x y z` 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: 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]` 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: Arguments:
- `--p1, --p2`: Area to delete from. Only mapblocks fully inside this area - `--p1, --p2`: Area containing mapblocks to delete. By default, only mapblocks
will be deleted. fully within this area will be deleted.
- `--invert`: Delete only mapblocks that are fully *outside* the given - `--invert`: Delete all mapblocks fully *outside* the given area. Use with
area. caution; you could erase a large portion of your world!
**Note:** Deleting mapblocks is *not* the same as filling them with air! Mapgen **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 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]` 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 Delete node metadata of certain nodes. Node inventories (such as chest/furnace
node inventories as well. contents) are also deleted.
Arguments: Arguments:
- `--node`: Name of node to modify. If not specified, the metadata of all - `--node`: Only delete metadata of nodes with the given name. If not
nodes will be deleted. specified, metadata will be deleted in all matching nodes.
- `--p1, --p2`: Area in which to delete metadata. If not specified, metadata - `--p1, --p2`: Area in which to delete metadata. If not specified, metadata
will be deleted everywhere. will be deleted everywhere.
- `--invert`: Only delete metadata *outside* the given area. - `--invert`: Only delete metadata *outside* the given area.

View File

@ -1,36 +1,35 @@
# MapEditr # MapEditr
TODO: Add a license. 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
MapEditr is a command-line tool for relatively fast manipulation of Minetest much more.
worlds. It can replace nodes, fill areas, combine parts of different worlds,
and much more.
This tool is functionally similar to [WorldEdit][1], but designed for large 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 operations that would be impractical to do within Minetest. Since it is mainly
optimized for speed, MapEditr is not as full-featured as in-game world editors optimized for speed, MapEditr lacks some of the more specialty features of
such as WorldEdit. WorldEdit.
MapEditr is originally based on [MapEdit][2], but rewritten in Rust, hence the MapEditr was originally based on [MapEdit][2], except written in Rust rather
added "r". Switching to a compiled language will make MapEditr more robust and than Python (hence the added "r"). Switching to a compiled language will make
easier to maintain in the future. MapEditr more robust and easier to maintain in the future.
[1]: https://github.com/Uberi/Minetest-WorldEdit [1]: https://github.com/Uberi/Minetest-WorldEdit
[2]: https://github.com/random-geek/MapEdit [2]: https://github.com/random-geek/MapEdit
## Installation ## Compilation/Installation
TODO: Pre-built binaries TODO: Pre-built binaries
To compile from source, you must have Rust installed first, which can be 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` `cargo build --release`
The `--release` flag is important, as it optimizes the generated executable, The `--release` flag is important, as it produces an optimized executable which
making it much faster. runs much faster than the default, unoptimized version.
[3]: https://www.rust-lang.org/tools/install [3]: https://www.rust-lang.org
## Usage ## Usage
@ -43,10 +42,14 @@ Some useful things you can do with MapEditr:
- Build extremely long walls and roads in seconds using `fill`. - Build extremely long walls and roads in seconds using `fill`.
- Combine multiple worlds or map saves with `overlay`. - Combine multiple worlds or map saves with `overlay`.
## License
TODO
## Acknowledgments ## Acknowledgments
The [Minetest][4] project has been rather important for the making of MapEdit/ The [Minetest][4] project has been rather important for the making of
MapEditr, for obvious reasons. MapEdit/MapEditr, for obvious reasons.
Some parts of the original MapEdit code were adapted from AndrejIT's 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 [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 std::collections::BTreeMap;
use crate::map_block::{MapBlock, NodeMetadataList}; use crate::map_block::{MapBlock, NodeMetadataList};
@ -115,7 +115,7 @@ pub fn clean_name_id_map(block: &mut MapBlock) {
} }
// Rebuild the name-ID map. // 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]; let mut map = vec![0u16; id_count];
for id in 0..id_count { for id in 0..id_count {
// Skip unused IDs. // 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)) fn to_cmd_line_args<'a>(tup: &(ArgType, &'a str))
-> Vec<Arg<'a, 'a>> -> Vec<Arg<'a, 'a>>
{ {
let arg = tup.0.clone(); let arg_type = tup.0.clone();
let help = tup.1; let help_msg = tup.1;
if let ArgType::Area(req) = arg { if let ArgType::Area(req) = arg_type {
return vec![ return vec![
Arg::with_name("p1") Arg::with_name("p1")
.long("p1") .long("p1")
@ -37,7 +37,7 @@ fn to_cmd_line_args<'a>(tup: &(ArgType, &'a str))
.value_names(&["x", "y", "z"]) .value_names(&["x", "y", "z"])
.required(req) .required(req)
.requires("p2") .requires("p2")
.help(help), .help(help_msg),
Arg::with_name("p2") Arg::with_name("p2")
.long("p2") .long("p2")
.allow_hyphen_values(true) .allow_hyphen_values(true)
@ -45,91 +45,78 @@ fn to_cmd_line_args<'a>(tup: &(ArgType, &'a str))
.value_names(&["x", "y", "z"]) .value_names(&["x", "y", "z"])
.required(req) .required(req)
.requires("p1") .requires("p1")
.help(help) .help(help_msg)
]; ];
} }
// TODO: Help is redundant. // TODO: Ensure arguments are correctly defined.
vec![match arg { let arg = match arg_type {
ArgType::Area(_) => unreachable!(),
ArgType::InputMapPath => ArgType::InputMapPath =>
Arg::with_name("input_map") Arg::with_name("input_map")
.required(true) .required(true),
.help(help),
ArgType::Area(_) => unreachable!(),
ArgType::Invert => ArgType::Invert =>
Arg::with_name("invert") Arg::with_name("invert")
.long("invert") .long("invert"),
.help(help),
ArgType::Offset(req) => ArgType::Offset(req) =>
Arg::with_name("offset") Arg::with_name("offset")
.long("offset") .long("offset")
.allow_hyphen_values(true) .allow_hyphen_values(true)
.number_of_values(3) .number_of_values(3)
.value_names(&["x", "y", "z"]) .value_names(&["x", "y", "z"])
.required(req) .required(req),
.help(help),
ArgType::Node(req) => { ArgType::Node(req) => {
let a = Arg::with_name("node") let a = Arg::with_name("node")
.required(req) .required(req);
.help(help); if req {
if !req {
a.long("node").takes_value(true)
} else {
a a
} else {
a.long("node").takes_value(true)
} }
}, },
ArgType::Nodes => ArgType::Nodes =>
Arg::with_name("nodes") Arg::with_name("nodes")
.long("nodes") .long("nodes")
.min_values(1) .min_values(1),
.help(help),
ArgType::NewNode => ArgType::NewNode =>
Arg::with_name("new_node") Arg::with_name("new_node")
.takes_value(true) .takes_value(true)
.required(true) .required(true),
.help(help),
ArgType::Object => ArgType::Object =>
Arg::with_name("object") Arg::with_name("object")
.long("obj") .long("obj")
.takes_value(true) .takes_value(true),
.help(help),
ArgType::Item => ArgType::Item =>
Arg::with_name("item") Arg::with_name("item")
.takes_value(true) .takes_value(true)
.required(true) .required(true),
.help(help),
ArgType::Items => ArgType::Items =>
Arg::with_name("items") Arg::with_name("items")
.long("items") .long("items")
.min_values(0) .min_values(0)
.max_values(1) .max_values(1),
.help(help),
ArgType::NewItem => ArgType::NewItem =>
Arg::with_name("new_item") Arg::with_name("new_item")
.takes_value(true) .takes_value(true),
.help(help),
ArgType::DeleteMeta => ArgType::DeleteMeta =>
Arg::with_name("delete_meta") Arg::with_name("delete_meta")
.long("deletemeta") .long("deletemeta"),
.help(help),
ArgType::DeleteItem => ArgType::DeleteItem =>
Arg::with_name("delete_item") Arg::with_name("delete_item")
.long("delete") .long("delete"),
.help(help),
ArgType::Key => ArgType::Key =>
Arg::with_name("key") Arg::with_name("key")
.takes_value(true) .takes_value(true)
.required(true) .required(true),
.help(help),
ArgType::Value => ArgType::Value =>
Arg::with_name("value") Arg::with_name("value")
.takes_value(true) .takes_value(true)
.required(true) .required(true),
.help(help),
ArgType::Param2Val => ArgType::Param2Val =>
Arg::with_name("param2_val") Arg::with_name("param2_val")
.required(true) .required(true),
.help(help), }.help(help_msg);
}]
vec![arg]
} }
@ -147,7 +134,9 @@ fn parse_cmd_line_args() -> anyhow::Result<InstArgs> {
let app = App::new("MapEditr") let app = App::new("MapEditr")
.about("Edits Minetest worlds/map databases.") .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!()) .version(crate_version!())
.author(crate_authors!()) .author(crate_authors!())
// TODO: Move map arg to subcommands? // TODO: Move map arg to subcommands?
@ -322,7 +311,7 @@ pub fn run_cmd_line() {
if forced_update == InstState::Querying if forced_update == InstState::Querying
|| (cur_state == InstState::Querying && timed_update_ready) || (cur_state == InstState::Querying && timed_update_ready)
{ {
eprint!("\rQuerying map blocks... {} found.", eprint!("\rQuerying mapblocks... {} found.",
status.get().blocks_total); status.get().blocks_total);
std::io::stdout().flush().unwrap(); std::io::stdout().flush().unwrap();
last_update = now; last_update = now;

View File

@ -1,14 +1,29 @@
use super::{Command, BLOCK_CACHE_SIZE}; use super::{Command, BLOCK_CACHE_SIZE};
use crate::{unwrap_or, opt_unwrap_or}; 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_database::MapDatabase;
use crate::map_block::{MapBlock, MapBlockError, is_valid_generated, use crate::map_block::{MapBlock, MapBlockError, is_valid_generated,
NodeMetadataList, NodeMetadataListExt}; NodeMetadataList, NodeMetadataListExt};
use crate::block_utils::{merge_blocks, merge_metadata, clean_name_id_map}; 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::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>>; 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 block_cache = CacheMap::with_capacity(BLOCK_CACHE_SIZE);
let mut tk = TimeKeeper::new();
inst.status.begin_editing(); inst.status.begin_editing();
for dst_key in dst_keys { for dst_key in dst_keys {
inst.status.inc_done(); inst.status.inc_done();
@ -93,40 +107,29 @@ fn clone(inst: &mut InstBundle) {
let dst_frag_rel = (src_frag_abs + offset) let dst_frag_rel = (src_frag_abs + offset)
.rel_block_overlap(dst_pos).unwrap(); .rel_block_overlap(dst_pos).unwrap();
{ merge_blocks(&src_block, &mut dst_block,
let _t = tk.get_timer("merge"); src_frag_rel, dst_frag_rel);
merge_blocks(&src_block, &mut dst_block, merge_metadata(&src_meta, &mut dst_meta,
src_frag_rel, dst_frag_rel); 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);
} }
clean_name_id_map(&mut dst_block);
*dst_block.metadata.get_mut() = dst_meta.serialize(dst_block.version); *dst_block.metadata.get_mut() = dst_meta.serialize(dst_block.version);
inst.db.set_block(dst_key, &dst_block.serialize()).unwrap(); inst.db.set_block(dst_key, &dst_block.serialize()).unwrap();
} }
inst.status.end_editing(); inst.status.end_editing();
tk.print(&inst.status);
} }
pub fn get_command() -> Command { pub fn get_command() -> Command {
Command { Command {
func: clone, func: clone,
verify_args: None, verify_args: Some(verify_args),
args: vec![ args: vec![
(ArgType::Area(true), "Area to clone"), (ArgType::Area(true), "Area to clone"),
(ArgType::Offset(true), "Vector to shift the area by") (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(); inst.status.begin_editing();
for key in keys { for key in keys {
// TODO: This is kind of inefficient seeming.
inst.status.inc_done(); inst.status.inc_done();
inst.db.delete_block(key).unwrap(); inst.db.delete_block(key).unwrap();
} }
@ -24,9 +23,10 @@ pub fn get_command() -> Command {
func: delete_blocks, func: delete_blocks,
verify_args: None, verify_args: None,
args: vec![ args: vec![
(ArgType::Area(true), "Area containing blocks to delete"), (ArgType::Area(true), "Area containing mapblocks to delete"),
(ArgType::Invert, "Delete all blocks *outside* the area") (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 node = inst.args.node.as_ref().map(to_bytes);
let keys = query_keys(&mut inst.db, &mut inst.status, 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(); inst.status.begin_editing();
let mut count: u64 = 0; let mut count: u64 = 0;
@ -19,7 +19,8 @@ fn delete_metadata(inst: &mut InstBundle) {
for key in keys { for key in keys {
inst.status.inc_done(); inst.status.inc_done();
let data = inst.db.get_block(key).unwrap(); 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_data = block.node_data.get_ref();
let node_id = node.as_deref().and_then(|n| block.nimap.get_id(n)); 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!( 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 block_corner = Vec3::from_block_key(key) * 16;
let mut to_delete = Vec::with_capacity(meta.len()); let mut to_delete = Vec::with_capacity(meta.len());
for (&idx, _) in &meta { for (&idx, _) in &meta {
let pos = Vec3::from_u16_key(idx); let abs_pos = Vec3::from_u16_key(idx) + block_corner;
let abs_pos = pos + block_corner;
if let Some(a) = inst.args.area { if let Some(a) = inst.args.area {
if a.contains(abs_pos) == inst.args.invert { if a.contains(abs_pos) == inst.args.invert {
@ -52,10 +53,10 @@ fn delete_metadata(inst: &mut InstBundle) {
} }
if !to_delete.is_empty() { if !to_delete.is_empty() {
count += to_delete.len() as u64;
for idx in &to_delete { for idx in &to_delete {
meta.remove(idx); meta.remove(idx);
} }
count += to_delete.len() as u64;
*block.metadata.get_mut() = meta.serialize(block.version); *block.metadata.get_mut() = meta.serialize(block.version);
inst.db.set_block(key, &block.serialize()).unwrap(); 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)); .map(|s| TwoWaySearcher::new(s));
let keys = query_keys(&mut inst.db, &mut inst.status, 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(); inst.status.begin_editing();
let mut count: u64 = 0; 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 node = inst.args.node.as_ref().map(to_bytes);
let keys = query_keys(&mut inst.db, &mut inst.status, 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(); inst.status.begin_editing();
let mut count: u64 = 0; let mut count: u64 = 0;

View File

@ -1,21 +1,28 @@
use super::Command; use super::Command;
use crate::unwrap_or; use crate::unwrap_or;
use crate::spatial::{Vec3, Area}; use crate::spatial::{Vec3, Area, InverseBlockIterator};
use crate::instance::{ArgType, InstBundle}; use crate::instance::{ArgType, InstBundle};
use crate::map_block::MapBlock; use crate::map_block::MapBlock;
use crate::block_utils::clean_name_id_map; use crate::block_utils::clean_name_id_map;
use crate::utils::{query_keys, to_bytes, fmt_big_num}; 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(); let nd = block.node_data.get_mut();
for z in area.min.z ..= area.max.z {
let z_start = z * 256; if invert {
for y in area.min.y ..= area.max.y { for i in InverseBlockIterator::new(area) {
let zy_start = z_start + y * 16; nd.nodes[i] = id;
for x in area.min.x ..= area.max.x { }
nd.nodes[(zy_start + x) as usize] = 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 node = to_bytes(inst.args.new_node.as_ref().unwrap());
let keys = query_keys(&mut inst.db, &mut inst.status, let keys = query_keys(&mut inst.db, &mut inst.status,
&[], Some(area), false, true); &[], Some(area), inst.args.invert, true);
inst.status.begin_editing(); inst.status.begin_editing();
@ -42,24 +49,23 @@ fn fill(inst: &mut InstBundle) {
continue; continue;
}); });
if area.contains_block(pos) { if area.contains_block(pos) != area.touches_block(pos) {
let nd = block.node_data.get_mut(); // Fill part of block
for x in &mut nd.nodes { let block_part = area.rel_block_overlap(pos).unwrap();
*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();
let fill_id = block.nimap.get_id(&node).unwrap_or_else(|| { let fill_id = block.nimap.get_id(&node).unwrap_or_else(|| {
let next = block.nimap.get_max_id().unwrap() + 1; let next = block.nimap.get_max_id().unwrap() + 1;
block.nimap.0.insert(next, node.to_vec()); block.nimap.0.insert(next, node.to_vec());
next 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); 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(); inst.db.set_block(key, &block.serialize()).unwrap();
@ -76,7 +82,8 @@ pub fn get_command() -> Command {
verify_args: None, verify_args: None,
args: vec![ args: vec![
(ArgType::Area(true), "Area to fill"), (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." help: "Fill the entire area with one node."
} }

View File

@ -1,7 +1,7 @@
use super::{Command, BLOCK_CACHE_SIZE}; use super::{Command, BLOCK_CACHE_SIZE};
use crate::{unwrap_or, opt_unwrap_or}; 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::instance::{ArgType, InstArgs, InstBundle};
use crate::map_database::MapDatabase; use crate::map_database::MapDatabase;
use crate::map_block::{MapBlock, MapBlockError, is_valid_generated, 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<()> { fn verify_args(args: &InstArgs) -> anyhow::Result<()> {
let offset_if_nonzero = if args.invert
args.offset.filter(|&off| off != Vec3::new(0, 0, 0)); && args.offset.filter(|&ofs| ofs != Vec3::new(0, 0, 0)).is_some()
if args.invert && offset_if_nonzero.is_some() { {
anyhow::bail!("Inverted selections cannot be offset."); 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(()) Ok(())
} }
@ -45,12 +58,12 @@ fn overlay_no_offset(inst: &mut InstBundle) {
if (!invert && area.contains_block(pos)) if (!invert && area.contains_block(pos))
|| (invert && !area.touches_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(); let data = idb.get_block(key).unwrap();
if is_valid_generated(&data) { if is_valid_generated(&data) {
db.set_block(key, &data).unwrap(); db.set_block(key, &data).unwrap();
} }
} else { // Copy part of map block } else { // Copy part of mapblock
let res = || -> Result<(), MapBlockError> { let res = || -> Result<(), MapBlockError> {
let dst_data = opt_unwrap_or!( let dst_data = opt_unwrap_or!(
db.get_block(key).ok() db.get_block(key).ok()
@ -91,7 +104,7 @@ fn overlay_no_offset(inst: &mut InstBundle) {
} }
} }
} else { } else {
// No area; copy whole map block. // No area; copy whole mapblock.
let data = idb.get_block(key).unwrap(); let data = idb.get_block(key).unwrap();
if is_valid_generated(&data) { if is_valid_generated(&data) {
db.set_block(key, &data).unwrap(); db.set_block(key, &data).unwrap();

View File

@ -1,146 +1,127 @@
use super::Command; 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::instance::{ArgType, InstArgs, InstBundle};
use crate::map_block::MapBlock; use crate::map_block::MapBlock;
use crate::time_keeper::TimeKeeper;
use crate::utils::{query_keys, to_bytes, fmt_big_num}; use crate::utils::{query_keys, to_bytes, fmt_big_num};
fn do_replace( fn do_replace(
block: &mut MapBlock, block: &mut MapBlock,
key: i64, key: i64,
search_id: u16, old_id: u16,
new_node: &[u8], new_node: &[u8],
area: Option<Area>, area: Option<Area>,
invert: bool, invert: bool
tk: &mut TimeKeeper
) -> u64 ) -> u64
{ {
let block_pos = Vec3::from_block_key(key); 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. // Replace nodes in a portion of a mapblock.
if area.is_some() && area.unwrap().contains_block(block_pos) != if area
area.unwrap().touches_block(block_pos) .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 node_area = area.unwrap().rel_block_overlap(block_pos).unwrap();
let mut new_replace_id = false; let (new_id, new_id_needed) = match block.nimap.get_id(new_node) {
let replace_id = block.nimap.get_id(new_node) Some(id) => (id, false),
.unwrap_or_else(|| { None => (block.nimap.get_max_id().unwrap() + 1, true)
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 nd = block.node_data.get_mut(); 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 { if invert {
old_node_present = true; for idx in InverseBlockIterator::new(node_area) {
} if nd.nodes[idx] == old_id {
idx += 1; 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 replacement ID is not in the name-ID map but was used, add it.
if new_replace_id && new_node_present { if new_id_needed && replaced > 0 {
block.nimap.0.insert(replace_id, new_node.to_vec()); block.nimap.0.insert(new_id, new_node.to_vec());
} }
// Search node was completely eliminated; shift IDs down. // If all instances of the old ID were replaced, remove the old ID.
if !old_node_present { if !nd.nodes.contains(&old_id) {
for i in 0 .. nd.nodes.len() { for node in &mut nd.nodes {
if nd.nodes[i] > search_id { *node -= (*node > old_id) as u16;
nd.nodes[i] -= 1;
}
} }
block.nimap.remove_shift(search_id); block.nimap.remove_shift(old_id);
} }
} }
// Replace nodes in whole map block. // Replace nodes in whole mapblock.
else { else {
// Block already contains replacement node, beware! // Block already contains replacement node, beware!
if let Some(mut replace_id) = block.nimap.get_id(new_node) { if let Some(mut new_id) = block.nimap.get_id(new_node) {
let _t = tk.get_timer("replace (non-unique replacement)");
// Delete unused ID from name-ID map and shift IDs down. // 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. // 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. // Map old node IDs to new node IDs.
let nd = block.node_data.get_mut(); let nd = block.node_data.get_mut();
for id in &mut nd.nodes { for id in &mut nd.nodes {
*id = if *id == search_id { *id = if *id == old_id {
count += 1; replaced += 1;
replace_id new_id
} else { } else {
*id - (*id > search_id) as u16 *id - (*id > old_id) as u16
}; };
} }
} }
// Block does not contain replacement node. // Block does not contain replacement node.
// Simply replace the node name in the name-ID map. // Simply replace the node name in the name-ID map.
else { else {
let _t = tk.get_timer("replace (unique replacement)");
let nd = block.node_data.get_ref(); let nd = block.node_data.get_ref();
for id in &nd.nodes { 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) { 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 new_node = to_bytes(inst.args.new_node.as_ref().unwrap());
let keys = query_keys(&mut inst.db, &inst.status, 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(); inst.status.begin_editing();
let mut count = 0; let mut count = 0;
let mut tk = TimeKeeper::new();
for key in keys { for key in keys {
let data = inst.db.get_block(key).unwrap(); let data = inst.db.get_block(key).unwrap();
let mut block = { let mut block = unwrap_or!(MapBlock::deserialize(&data),
let _t = tk.get_timer("decode"); { inst.status.inc_failed(); continue; });
MapBlock::deserialize(&data).unwrap()
};
if let Some(search_id) = block.nimap.get_id(&node) { if let Some(old_id) = block.nimap.get_id(&old_node) {
count += do_replace(&mut block, key, search_id, &new_node, count += do_replace(&mut block, key, old_id, &new_node,
inst.args.area, inst.args.invert, &mut tk); inst.args.area, inst.args.invert);
let new_data = { let new_data = block.serialize();
let _t = tk.get_timer("encode");
block.serialize()
};
inst.db.set_block(key, &new_data).unwrap(); inst.db.set_block(key, &new_data).unwrap();
} }
inst.status.inc_done(); inst.status.inc_done();
} }
// tk.print();
inst.status.end_editing(); inst.status.end_editing();
inst.status.log_info(format!("{} nodes replaced.", fmt_big_num(count))); 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) { fn set_meta_var(inst: &mut InstBundle) {
// TODO: Bytes input
let key = to_bytes(inst.args.key.as_ref().unwrap()); let key = to_bytes(inst.args.key.as_ref().unwrap());
let value = to_bytes(inst.args.value.as_ref().unwrap()); let value = to_bytes(inst.args.value.as_ref().unwrap());
let nodes: Vec<_> = inst.args.nodes.iter().map(to_bytes).collect(); let nodes: Vec<_> = inst.args.nodes.iter().map(to_bytes).collect();

View File

@ -1,81 +1,89 @@
use super::Command; 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::instance::{ArgType, InstArgs, InstBundle};
use crate::map_block::MapBlock; use crate::map_block::MapBlock;
use crate::utils::{query_keys, to_bytes, to_slice, fmt_big_num}; 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 nd = block.node_data.get_mut();
let mut count = 0; let mut count = 0;
for z in area.min.z ..= area.max.z {
let z_start = z * 256; if invert {
for y in area.min.y ..= area.max.y { if let Some(id) = node_id {
let zy_start = z_start + y * 16; for idx in InverseBlockIterator::new(area) {
for x in area.min.x ..= area.max.x { if nd.nodes[idx] == id {
let i = (zy_start + x) as usize; nd.param2[idx] = val;
if nd.nodes[i] == id {
nd.param2[i] = val;
count += 1; 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 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) { fn set_param2(inst: &mut InstBundle) {
let param2_val = inst.args.param2_val.unwrap(); let param2_val = inst.args.param2_val.unwrap();
let node = inst.args.node.as_ref().map(to_bytes); let node = inst.args.node.as_ref().map(to_bytes);
let keys = query_keys(&mut inst.db, &mut inst.status, 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(); inst.status.begin_editing();
let mut count: u64 = 0; let mut count: u64 = 0;
use crate::time_keeper::TimeKeeper;
let mut tk = TimeKeeper::new();
for key in keys { for key in keys {
inst.status.inc_done(); inst.status.inc_done();
let pos = Vec3::from_block_key(key); let pos = Vec3::from_block_key(key);
let data = inst.db.get_block(key).unwrap(); 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)); let node_id = node.as_ref().and_then(|n| block.nimap.get_id(n));
if inst.args.node.is_some() && node_id.is_none() { if inst.args.node.is_some() && node_id.is_none() {
// Node not found in this map block. // Node not found in this mapblock.
continue; continue;
} }
let nd = block.node_data.get_mut(); let nd = block.node_data.get_mut();
if let Some(area) = inst.args.area 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 { // Modify part of block
let overlap = area.rel_block_overlap(pos).unwrap(); let block_part = area.rel_block_overlap(pos).unwrap();
if let Some(nid) = node_id { let _t = tk.get_timer("set_param2_partial");
count += count += set_param2_partial(&mut block,
set_in_area_node(&mut block, overlap, nid, param2_val); block_part, inst.args.invert, node_id, param2_val);
} else {
set_in_area(&mut block, overlap, param2_val);
count += overlap.volume();
}
} else { // Modify whole block } else { // Modify whole block
if let Some(nid) = node_id { if let Some(nid) = node_id {
for i in 0 .. nd.param2.len() { for i in 0 .. nd.param2.len() {
@ -85,9 +93,7 @@ fn set_param2(inst: &mut InstBundle) {
} }
} }
} else { } else {
for x in &mut nd.param2 { nd.param2.fill(param2_val);
*x = param2_val;
}
count += nd.param2.len() as u64; count += nd.param2.len() as u64;
} }
} }
@ -96,6 +102,7 @@ fn set_param2(inst: &mut InstBundle) {
} }
inst.status.end_editing(); inst.status.end_editing();
tk.print(&mut inst.status);
inst.status.log_info(format!("{} nodes set.", fmt_big_num(count))); 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), verify_args: Some(verify_args),
args: vec![ args: vec![
(ArgType::Area(false), "Area in which to set param2 values"), (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::Node(false), "Node to set param2 values of"),
(ArgType::Param2Val, "New param2 value") (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 anyhow::Context;
use crate::spatial::{Vec3, Area}; use crate::spatial::{Vec3, Area, MAP_LIMIT};
use crate::map_database::MapDatabase; use crate::map_database::MapDatabase;
use crate::commands; use crate::commands;
@ -193,6 +193,27 @@ fn status_channel() -> (StatusServer, StatusClient) {
fn verify_args(args: &InstArgs) -> anyhow::Result<()> { 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 { fn is_valid_name(name: &str) -> bool {
if name == "air" || name == "ignore" { if name == "air" || name == "ignore" {
true true
@ -205,29 +226,12 @@ fn verify_args(args: &InstArgs) -> anyhow::Result<()> {
let mod_name = &name[..delim]; let mod_name = &name[..delim];
let item_name = &name[delim + 1..]; 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 == '_')) !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'))
.is_some() .is_none()
|| item_name.find(|c: char| && item_name.find(|c: char|
!(c.is_ascii_alphanumeric() || c == '_')) !(c.is_ascii_alphanumeric() || c == '_'))
.is_some() .is_none()
{
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);
} }
} }
@ -246,6 +250,13 @@ fn verify_args(args: &InstArgs) -> anyhow::Result<()> {
verify_name!(args.new_node, "Invalid node name: {}"); verify_name!(args.new_node, "Invalid node name: {}");
verify_name!(args.object, "Invalid object name: {}"); verify_name!(args.object, "Invalid object name: {}");
verify_name!(args.item, "Invalid item 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(()) Ok(())
} }

View File

@ -80,6 +80,22 @@ impl Area {
(self.max.z - self.min.z + 1) as u64 (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 { pub fn contains(&self, pos: Vec3) -> bool {
self.min.x <= pos.x && pos.x <= self.max.x self.min.x <= pos.x && pos.x <= self.max.x
&& self.min.y <= pos.y && pos.y <= self.max.y && 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))); 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] #[test]
fn test_area_containment() { fn test_area_containment() {
let area = Area::new(Vec3::new(-1, -32, 16), Vec3::new(30, -17, 54)); let area = Area::new(Vec3::new(-1, -32, 16), Vec3::new(30, -17, 54));

View File

@ -1,5 +1,112 @@
mod vec3; mod vec3;
mod area; mod area;
pub use vec3::Vec3; pub use vec3::{MAP_LIMIT, Vec3};
pub use area::Area; 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)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct Vec3 { pub struct Vec3 {
pub x: i32, pub x: i32,
@ -39,7 +42,7 @@ impl Vec3 {
} }
pub fn is_valid_block_pos(&self) -> bool { 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.x && self.x <= LIMIT
&& -LIMIT <= self.y && self.y <= LIMIT && -LIMIT <= self.y && self.y <= LIMIT
@ -47,7 +50,7 @@ impl Vec3 {
} }
pub fn is_valid_node_pos(&self) -> bool { 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.x && self.x <= LIMIT
&& -LIMIT <= self.y && self.y <= LIMIT && -LIMIT <= self.y && self.y <= LIMIT