diff --git a/Manual.md b/Manual.md index 5a2f74f..313951d 100644 --- a/Manual.md +++ b/Manual.md @@ -74,6 +74,22 @@ area. will be invoked where the blocks were deleted, and this sometimes causes terrain glitches. +### deleteobjects + +Usage: `deleteobjects [--obj ] [--items] [--p1 x y z] [--p2 x y z] [--invert]` + +Delete objects (entities) of a certain name and/or within a certain area. + +Arguments: + +- `--obj`: Name of object to search for, e.g. "boats:boat". If not specified, +all objects will be deleted. +- `--items`: Search for only item entities (dropped items). If this flag is +set, `--obj` can optionally be used to specify an item name. +- `--p1, --p2`: Area in which to delete objects. If not specified, objects will +be deleted across the entire map. +- `--invert`: Delete objects *outside* the given area. + ### fill Usage: `fill --p1 x y z --p2 x y z [--invert] ` @@ -154,6 +170,21 @@ all nodes will be set. not specified. - `--invert`: Only set param2 *outside* the given area. +### vacuum + +Usage: `vacuum` + +Vacuums the database. This reduces the size of the database, but may take a +long time. + +All this does is perform an SQLite `VACUUM` command. This shrinks and optimizes +the database by efficiently "repacking" all mapblocks. No map data is changed +or deleted. + +**Note:** Because data is copied into another file, vacuum could require +as much free disk space as is already occupied by the map. For example, if +map.sqlite is 10 GB, make sure you have **at least 10 GB** of free space! + @@ -216,28 +247,3 @@ Arguments: - **`--searchnode`**: Name of node to search for. If not specified, the node timers of all nodes will be deleted. - **`--p1, --p2`**: Area in which to delete node timers. Required if `searchnode` is not specified. - **`--invert`**: Only delete node timers *outside* the given area. - -### `deleteobjects` - -**Usage:** `deleteobjects [--searchobj ] [--items] [--p1 x y z] [--p2 x y z] [--invert]` - -Delete static objects of a certain name and/or within a certain area. - -Arguments: - -- **`--searchobj`**: Name of object to search for, e.g. "boats:boat". If not specified, all objects will be deleted. -- **`--items`**: Search for only item entities (dropped items). `searchobj` determines the item name, if specified. -- **`--p1, --p2`**: Area in which to delete objects. If not specified, objects will be deleted across the entire map. -- **`--invert`**: Only delete objects *outside* the given area. - -### `vacuum` - -**Usage:** `vacuum` - -Vacuums the database. This reduces the size of the database, but may take a long time. - -All this does is perform an SQLite `VACUUM` command. This shrinks and optimizes the database by efficiently "repacking" all mapblocks. -No map data is changed or deleted. - -**Note:** Because data is copied into another file, this command could require as much free disk space as is already occupied by the map. -For example, if your database is 10 GB, make sure you have **at least 10 GB** of free space! diff --git a/src/block_utils.rs b/src/block_utils.rs index 7656cab..e999f25 100644 --- a/src/block_utils.rs +++ b/src/block_utils.rs @@ -111,7 +111,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/commands/delete_objects.rs b/src/commands/delete_objects.rs index 3f59b88..4b0cb10 100644 --- a/src/commands/delete_objects.rs +++ b/src/commands/delete_objects.rs @@ -1,7 +1,8 @@ use super::Command; +use crate::spatial::Area; use crate::instance::{ArgType, InstBundle}; -use crate::map_block::{MapBlock, LuaEntityData}; +use crate::map_block::{MapBlock, StaticObject, LuaEntityData}; use crate::utils::{query_keys, fmt_big_num}; use memmem::{Searcher, TwoWaySearcher}; @@ -17,21 +18,45 @@ macro_rules! unwrap_or { } +#[inline] +fn can_delete( + obj: &StaticObject, + area: &Option, + invert: bool +) -> bool { + // Check area requirements + if let Some(a) = area { + const DIV_FAC: i32 = 10_000; + let rounded_pos = obj.f_pos.map( + |v| (v - DIV_FAC / 2).div_euclid(DIV_FAC)); + if a.contains(rounded_pos) == invert { + return false; + } + } + + true +} + + fn delete_objects(inst: &mut InstBundle) { - const ITEM_ENT_NAME: &'static [u8] = b"__builtin:item"; + const ITEM_ENT_NAME: &[u8] = b"__builtin:item"; let search_obj = if inst.args.items { - Some(String::from_utf8(ITEM_ENT_NAME.to_vec()).unwrap()) + Some(ITEM_ENT_NAME.to_owned()) } else { - inst.args.object.clone() + inst.args.object.as_ref().map(|s| s.as_bytes().to_owned()) }; let keys = query_keys(&mut inst.db, &mut inst.status, - search_obj.clone(), inst.args.area, inst.args.invert, true); + search_obj.as_deref(), inst.args.area, inst.args.invert, true); + + let search_item = search_obj.as_ref().filter(|_| inst.args.items).map(|s| + format!( + "[\"itemstring\"] = \"{}\"", + String::from_utf8(s.to_owned()).unwrap() + ).into_bytes() + ); + let item_searcher = search_item.as_ref().map(|s| TwoWaySearcher::new(s)); inst.status.begin_editing(); - - let item_searcher = search_obj.as_ref().filter(|_| inst.args.items) - .map(|s| TwoWaySearcher::new(format!("[itemstring]=\"{}\"", s))); - let mut count: u64 = 0; for key in keys { inst.status.inc_done(); @@ -42,39 +67,15 @@ fn delete_objects(inst: &mut InstBundle) { for i in (0..block.static_objects.list.len()).rev() { let obj = &block.static_objects.list[i]; - // Check area requirements - if let Some(area) = inst.args.area { - const DIV_FAC: i32 = 10_000; - let rounded_pos = obj.f_pos.map( - |v| (v - DIV_FAC / 2).div_euclid(DIV_FAC)); - if area.contains(rounded_pos) == inst.args.invert { - continue; - } + if can_delete( + &block.static_objects.list[i], + &inst.args.area, + inst.args.invert + ) { + block.static_objects.list.remove(i); + modified = true; + count += 1; } - - // Check name requirements - let le_data = unwrap_or!(LuaEntityData::deserialize(&obj), - continue); - if inst.args.items { - if le_data.name != ITEM_ENT_NAME { - continue; - } - if let Some(searcher) = &item_searcher { - if searcher.search_in(&le_data.data).is_none() { - continue; - } - } - } else { - if let Some(sobj) = &search_obj { - if le_data.name != sobj.as_bytes() { - continue; - } - } - } - - block.static_objects.list.remove(i); - modified = true; - count += 1; } if modified { diff --git a/src/commands/fill.rs b/src/commands/fill.rs index 35deb08..cca7688 100644 --- a/src/commands/fill.rs +++ b/src/commands/fill.rs @@ -23,7 +23,7 @@ fn fill_area(block: &mut MapBlock, area: Area, id: u16) { fn fill(inst: &mut InstBundle) { let area = inst.args.area.unwrap(); - let node = inst.args.new_node.clone().unwrap(); + let node = inst.args.new_node.as_ref().unwrap().as_bytes().to_owned(); let keys = query_keys(&mut inst.db, &mut inst.status, None, Some(area), false, true); diff --git a/src/commands/replace_nodes.rs b/src/commands/replace_nodes.rs index e032d2d..43d7f8e 100644 --- a/src/commands/replace_nodes.rs +++ b/src/commands/replace_nodes.rs @@ -13,7 +13,7 @@ fn do_replace( block: &mut MapBlock, key: i64, search_id: u16, - new_node: &str, + new_node: &[u8], area: Option, invert: bool, tk: &mut TimeKeeper @@ -113,10 +113,10 @@ fn do_replace( fn replace_nodes(inst: &mut InstBundle) { - let node = inst.args.node.clone().unwrap(); - let new_node = inst.args.new_node.clone().unwrap(); + let node = inst.args.node.as_ref().unwrap().as_bytes(); + let new_node = inst.args.new_node.as_ref().unwrap().as_bytes(); let keys = query_keys(&mut inst.db, &inst.status, - Some(node.clone()), inst.args.area, inst.args.invert, true); + Some(node), inst.args.area, inst.args.invert, true); inst.status.begin_editing(); let mut count = 0; diff --git a/src/commands/set_param2.rs b/src/commands/set_param2.rs index 3314d9a..a370d3d 100644 --- a/src/commands/set_param2.rs +++ b/src/commands/set_param2.rs @@ -1,7 +1,7 @@ use super::Command; use crate::spatial::{Vec3, Area, area_rel_block_overlap, area_contains_block}; -use crate::instance::{ArgType, InstBundle}; +use crate::instance::{ArgType, InstArgs, InstBundle}; use crate::map_block::{MapBlock}; use crate::utils::{query_keys, fmt_big_num}; @@ -42,12 +42,11 @@ fn set_in_area(block: &mut MapBlock, area: Area, val: u8) { fn set_param2(inst: &mut InstBundle) { - // TODO: Actually verify! - assert!(inst.args.area.is_some() || inst.args.node.is_some()); let param2_val = inst.args.param2_val.unwrap(); + let node = inst.args.node.as_ref().map(|s| s.as_bytes().to_owned()); let keys = query_keys(&mut inst.db, &mut inst.status, - inst.args.node.clone(), inst.args.area, false, true); + node.as_deref(), inst.args.area, false, true); inst.status.begin_editing(); @@ -59,8 +58,7 @@ fn set_param2(inst: &mut InstBundle) { let data = inst.db.get_block(key).unwrap(); let mut block = MapBlock::deserialize(&data).unwrap(); - let node_id = inst.args.node.as_deref() - .and_then(|node| block.nimap.get_id(&node)); + 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. continue; @@ -103,10 +101,17 @@ fn set_param2(inst: &mut InstBundle) { } +fn verify_args(args: &InstArgs) -> anyhow::Result<()> { + anyhow::ensure!(args.area.is_some() || args.node.is_some(), + "An area and/or node must be provided."); + Ok(()) +} + + pub fn get_command() -> Command { Command { func: set_param2, - verify_args: None, + verify_args: Some(verify_args), args: vec![ (ArgType::Area(false), "Area in which to set param2 values"), (ArgType::Node(false), "Node to set param2 values of"), diff --git a/src/map_block/name_id_map.rs b/src/map_block/name_id_map.rs index 7ae94a7..b4be105 100644 --- a/src/map_block/name_id_map.rs +++ b/src/map_block/name_id_map.rs @@ -8,7 +8,7 @@ use super::*; #[derive(Debug)] pub struct NameIdMap { // Use a BTreeMap instead of a HashMap to preserve the order of IDs. - pub map: BTreeMap, + pub map: BTreeMap>, } impl NameIdMap { @@ -26,8 +26,7 @@ impl NameIdMap { for _ in 0 .. count { let id = data.read_u16::()?; let name = read_string16(data)?; - let string = String::from_utf8_lossy(&name).into_owned(); - map.insert(id, string); + map.insert(id, name); } Ok(Self {map}) @@ -39,14 +38,15 @@ impl NameIdMap { for (id, name) in &self.map { out.write_u16::(*id).unwrap(); - write_string16(out, name.as_bytes()); + write_string16(out, name); } } #[inline] - pub fn get_id(&self, name: &str) -> Option { - self.map.iter() - .find_map(|(&k, v)| if v == name { Some(k) } else { None }) + pub fn get_id(&self, name: &[u8]) -> Option { + self.map.iter().find_map(|(&k, v)| + if v.as_slice() == name { Some(k) } else { None } + ) } #[inline] @@ -55,8 +55,8 @@ impl NameIdMap { } #[inline] - pub fn insert(&mut self, id: u16, name: &str) { - self.map.insert(id, name.to_string()); + pub fn insert(&mut self, id: u16, name: &[u8]) { + self.map.insert(id, name.to_owned()); } /// Remove the name at a given ID and shift down values above it. diff --git a/src/utils.rs b/src/utils.rs index deff099..ff3f56f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -11,7 +11,7 @@ use crate::spatial::{Area, Vec3}; pub fn query_keys( db: &mut MapDatabase, status: &StatusServer, - search_str: Option, + search_str: Option<&[u8]>, area: Option, invert: bool, include_partial: bool @@ -22,8 +22,8 @@ pub fn query_keys( // This will break if the name-ID map format changes. let search_bytes = search_str.map(|s| { let mut res = Vec::new(); - res.write_u16::(s.as_bytes().len() as u16).unwrap(); - res.extend(s.as_bytes()); + res.write_u16::(s.len() as u16).unwrap(); + res.extend(s); res }); let data_searcher = search_bytes.as_ref().map(|b| {